From 2c424294afb566f71b571ca0ec7fdad4f9e638c6 Mon Sep 17 00:00:00 2001 From: Joey Stanford Date: Fri, 11 Sep 2026 05:27:34 -0600 Subject: [PATCH 1/6] fix(meshcore): keep silent-bulk circuit open after incremental drain Successful syncNextMessage was clearing the getWaitingMessages timeout breaker, so bulk was retried and timed out in a loop until reconnect. --- .../meshcore/meshcoreConnSideEffects.test.ts | 17 +++++++++++++++-- .../hooks/meshcore/meshcoreConnSideEffects.ts | 5 +++-- .../lib/meshcoreWaitingMessagesDrain.test.ts | 16 ++++++++++++++++ .../lib/meshcoreWaitingMessagesDrain.ts | 10 ++++++++-- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts b/src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts index b4581b5e9..aca6888c5 100644 --- a/src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts +++ b/src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts @@ -11,7 +11,9 @@ import * as meshcoreRepeaterRpcInFlight from '@/renderer/lib/meshcoreRepeaterRpc import { meshcoreChatStubNodeIdFromDisplayName } from '@/renderer/lib/meshcoreUtils'; import { beginMeshcoreSilentBulkAttempt, + resetMeshcoreWaitingMessagesDrainSchedule, resetMeshcoreWaitingMessagesDrainState, + shouldSkipMeshcoreSilentBulkGetWaitingMessages, } from '@/renderer/lib/meshcoreWaitingMessagesDrain'; import type { DomainEvent } from '@/renderer/lib/protocols/Protocol'; import { @@ -484,6 +486,7 @@ describe('attachMeshcoreConnSideEffects', () => { expect(h.conn.getWaitingMessages).toHaveBeenCalledTimes( MESHCORE_WAITING_MESSAGES_SILENT_BULK_TIMEOUT_TRIP, ); + expect(shouldSkipMeshcoreSilentBulkGetWaitingMessages()).toBe(true); vi.mocked(h.conn.getWaitingMessages).mockClear(); const skipped = h.ctx.processWaitingMessagesRef.current?.({ showSyncBanner: false }); @@ -493,12 +496,22 @@ describe('attachMeshcoreConnSideEffects', () => { expect(h.conn.getWaitingMessages).not.toHaveBeenCalled(); expect(h.syncNextMessage).toHaveBeenCalled(); expect(h.handleConnectionLost).not.toHaveBeenCalled(); + // Incremental success must leave the breaker open (no bulk re-probe until reconnect). + expect(shouldSkipMeshcoreSilentBulkGetWaitingMessages()).toBe(true); vi.mocked(h.conn.getWaitingMessages).mockClear(); vi.mocked(h.conn.getWaitingMessages).mockResolvedValue([]); - const retried = h.ctx.processWaitingMessagesRef.current?.({ showSyncBanner: false }); + const stillSkipped = h.ctx.processWaitingMessagesRef.current?.({ showSyncBanner: false }); await vi.runAllTimersAsync(); - await retried; + await stillSkipped; + expect(h.conn.getWaitingMessages).not.toHaveBeenCalled(); + expect(shouldSkipMeshcoreSilentBulkGetWaitingMessages()).toBe(true); + + resetMeshcoreWaitingMessagesDrainSchedule(); + expect(shouldSkipMeshcoreSilentBulkGetWaitingMessages()).toBe(false); + const afterReconnect = h.ctx.processWaitingMessagesRef.current?.({ showSyncBanner: false }); + await vi.runAllTimersAsync(); + await afterReconnect; expect(h.conn.getWaitingMessages).toHaveBeenCalledTimes(1); }); diff --git a/src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts b/src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts index 514828889..2b896090b 100644 --- a/src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts +++ b/src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts @@ -332,8 +332,9 @@ async function drainWaitingMessagesSilent( const preferIncremental = opts?.incrementalOnly || shouldPreferMeshcoreSilentIncrementalDrain(deps.connectionType); if (preferIncremental) { - const retrieved = await drainWaitingMessagesIncremental(conn, state, deps, syncNextTimeoutMs); - if (retrieved) noteMeshcoreSilentBulkSuccess(); + // Incremental success must not clear the silent-bulk timeout circuit — only a real + // getWaitingMessages success (below) or disconnect/teardown reset may re-probe bulk. + await drainWaitingMessagesIncremental(conn, state, deps, syncNextTimeoutMs); return; } diff --git a/src/renderer/lib/meshcoreWaitingMessagesDrain.test.ts b/src/renderer/lib/meshcoreWaitingMessagesDrain.test.ts index 1b0324c5d..a9585c15f 100644 --- a/src/renderer/lib/meshcoreWaitingMessagesDrain.test.ts +++ b/src/renderer/lib/meshcoreWaitingMessagesDrain.test.ts @@ -7,6 +7,7 @@ import { awaitMeshcoreWaitingMessagesDrainIdle, beginMeshcoreSilentBulkAttempt, endMeshcoreSilentBulkCliPreempt, + getMeshcoreSilentBulkDrainSnapshot, isMeshcoreCompanionDrainDeferred, isMeshcoreSilentBulkAttemptCurrent, isMeshcoreSyncNextMessageTimeoutError, @@ -404,6 +405,21 @@ describe('silent bulk timeout circuit breaker', () => { expect(shouldSkipMeshcoreSilentBulkGetWaitingMessages()).toBe(false); }); + it('stays open when incremental drain succeeds without noteMeshcoreSilentBulkSuccess', () => { + for (let i = 0; i < MESHCORE_WAITING_MESSAGES_SILENT_BULK_TIMEOUT_TRIP; i += 1) { + noteMeshcoreSilentBulkTimeout(); + } + expect(shouldSkipMeshcoreSilentBulkGetWaitingMessages()).toBe(true); + expect(shouldPreferMeshcoreSilentIncrementalDrain('ble')).toBe(true); + // Prefer-incremental path must not call noteMeshcoreSilentBulkSuccess — circuit stays open + // until reconnect reset or a real getWaitingMessages success. + expect(shouldSkipMeshcoreSilentBulkGetWaitingMessages()).toBe(true); + expect(getMeshcoreSilentBulkDrainSnapshot()).toEqual({ + silentBulkSkipped: true, + silentBulkTimeoutStreak: MESHCORE_WAITING_MESSAGES_SILENT_BULK_TIMEOUT_TRIP, + }); + }); + it('resets on drain state reset (reconnect)', () => { for (let i = 0; i < MESHCORE_WAITING_MESSAGES_SILENT_BULK_TIMEOUT_TRIP; i += 1) { noteMeshcoreSilentBulkTimeout(); diff --git a/src/renderer/lib/meshcoreWaitingMessagesDrain.ts b/src/renderer/lib/meshcoreWaitingMessagesDrain.ts index 17842b9bc..9465a2c39 100644 --- a/src/renderer/lib/meshcoreWaitingMessagesDrain.ts +++ b/src/renderer/lib/meshcoreWaitingMessagesDrain.ts @@ -26,7 +26,10 @@ let lastMsgWaitingEventAt = 0; let silentBulkAttemptId = 0; /** Consecutive silent-bulk getWaitingMessages timeouts on this connection. */ let silentBulkTimeoutStreak = 0; -/** Once tripped, skip bulk and go straight to syncNextMessage until reconnect/success. */ +/** + * Once tripped, skip bulk and go straight to syncNextMessage until reconnect/teardown + * reset or a real successful getWaitingMessages — not incremental syncNextMessage success. + */ let silentBulkSkipped = false; /** * CLI reply path: skip bulk while awaiting CLI_DATA (cleared when CLI hold ends). @@ -171,7 +174,10 @@ export function shouldPreferMeshcoreSilentIncrementalDrain( return connectionType === 'tcp' || shouldSkipMeshcoreSilentBulkGetWaitingMessages(); } -/** Record a successful silent bulk drain (including empty queue). */ +/** + * Record a successful silent bulk getWaitingMessages (including empty queue). + * Do not call this after incremental syncNextMessage — that would re-open bulk too soon. + */ export function noteMeshcoreSilentBulkSuccess(): void { silentBulkTimeoutStreak = 0; silentBulkSkipped = false; From 27fb3926ff9d92cd2d587709f710f1819df8e51a Mon Sep 17 00:00:00 2001 From: Joey Stanford Date: Fri, 11 Sep 2026 05:33:24 -0600 Subject: [PATCH 2/6] fix(nomad): give /media proxyGet the Nomad page timeout cap In-page WebP fetches were aborted at the default 10s while page/file already used the 185s Link budget, so BLE images never finished loading. --- src/main/reticulum-proxy-path.test.ts | 7 +++++++ src/main/reticulum-proxy-path.ts | 3 ++- src/shared/reticulumNomadTimeouts.test.ts | 2 +- src/shared/reticulumNomadTimeouts.ts | 4 ++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/reticulum-proxy-path.test.ts b/src/main/reticulum-proxy-path.test.ts index b4461187b..1d55421e5 100644 --- a/src/main/reticulum-proxy-path.test.ts +++ b/src/main/reticulum-proxy-path.test.ts @@ -73,6 +73,13 @@ describe('reticulumProxyGetTimeoutMs', () => { ).toBe(185_000); }); + it('uses flat Nomad proxy cap for media fetches', () => { + expect( + reticulumProxyGetTimeoutMs('/api/v1/nomadnetwork/media/abc?path=%2Fmedia%2Fdemo.webp'), + ).toBe(185_000); + expect(reticulumProxyGetTimeoutMs('/api/v1/nomadnetwork/media/abc')).toBe(185_000); + }); + it('uses default timeout for other GET routes', () => { expect(reticulumProxyGetTimeoutMs('/api/v1/nomadnetwork/nodes')).toBe(10_000); }); diff --git a/src/main/reticulum-proxy-path.ts b/src/main/reticulum-proxy-path.ts index c82b9bab1..4fb9e1a0f 100644 --- a/src/main/reticulum-proxy-path.ts +++ b/src/main/reticulum-proxy-path.ts @@ -34,7 +34,8 @@ function computeReticulumProxyGetTimeoutMs(apiPath: string): number { const normalized = pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`; if ( normalized.includes('/api/v1/nomadnetwork/page/') || - normalized.includes('/api/v1/nomadnetwork/file/') + normalized.includes('/api/v1/nomadnetwork/file/') || + normalized.includes('/api/v1/nomadnetwork/media/') ) { return nomadPageProxyTimeoutMsFromApiPath(trimmed); } diff --git a/src/shared/reticulumNomadTimeouts.test.ts b/src/shared/reticulumNomadTimeouts.test.ts index ce0538b35..817a4f1fd 100644 --- a/src/shared/reticulumNomadTimeouts.test.ts +++ b/src/shared/reticulumNomadTimeouts.test.ts @@ -19,7 +19,7 @@ describe('reticulumNomadTimeouts', () => { expect(nomadPageOverallTimeoutSecs('rf', 32)).toBe(180); }); - it('uses a flat IPC proxy cap for all Nomad page/file paths', () => { + it('uses a flat IPC proxy cap for all Nomad page/file/media paths', () => { expect(NOMAD_PROXY_GET_TIMEOUT_MS).toBe(185_000); expect( nomadPageProxyTimeoutMsFromApiPath( diff --git a/src/shared/reticulumNomadTimeouts.ts b/src/shared/reticulumNomadTimeouts.ts index a8d7ad0be..9e42c859f 100644 --- a/src/shared/reticulumNomadTimeouts.ts +++ b/src/shared/reticulumNomadTimeouts.ts @@ -31,7 +31,7 @@ export const NOMAD_RF_TRANSFER_GRACE_SECS = 30; export const NOMAD_RF_MAX_OVERALL_SECS = 180; /** - * Main-process IPC proxy AbortSignal timeout for Nomad page/file GETs. + * Main-process IPC proxy AbortSignal timeout for Nomad page/file/media GETs. * Sidecar LinkClient already enforces the egress×hops deadline; main must not * cut off early when UI hops are stale (use a flat cap above the RF max). */ @@ -70,7 +70,7 @@ export function nomadPageOverallTimeoutSecs( } /** - * IPC proxy timeout for Nomad page/file fetches. + * IPC proxy timeout for Nomad page/file/media fetches. * Always the flat {@link NOMAD_PROXY_GET_TIMEOUT_MS} so stale renderer hops * cannot abort before the sidecar's own LinkClient deadline. */ From d5cef643d85ae6ce17d53c5dcab49ae970c70d54 Mon Sep 17 00:00:00 2001 From: Joey Stanford Date: Fri, 11 Sep 2026 05:46:52 -0600 Subject: [PATCH 3/6] chore(ratspeak): float mains and overlay open feature PRs Drop SHA pins so local and CI always float origin/main, then carry rsReticulum#26 and rsLXMF#7 as overlays so NomadNet /media keeps building. --- .github/workflows/flatpak.yaml | 4 - .github/workflows/reticulum-sidecar.yaml | 10 +- AGENTS.md | 2 +- docs/ci-cd.md | 16 +- docs/development-environment.md | 2 +- docs/reticulum.md | 2 +- reticulum-sidecar/README.md | 4 +- reticulum-sidecar/patches/README.md | 89 +++- .../rsLXMF-file-attachments-list.patch | 181 +++++++ ...sReticulum-reply-file-query-metadata.patch | 504 ++++++++++++++++++ scripts/apply-rsLXMF-file-attachments-list.sh | 40 ++ ...y-rsReticulum-reply-file-query-metadata.sh | 42 ++ scripts/clone-ratspeak-stack.sh | 10 +- scripts/clone-ratspeak-stack.test.mjs | 6 +- scripts/lib/ratspeak-overlay-apply-list.sh | 2 + scripts/ratspeak-stack-ci-pins.env | 12 - scripts/update.sh | 72 +-- scripts/update.test.mjs | 89 +--- 18 files changed, 879 insertions(+), 208 deletions(-) create mode 100644 reticulum-sidecar/patches/rsLXMF-file-attachments-list.patch create mode 100644 reticulum-sidecar/patches/rsReticulum-reply-file-query-metadata.patch create mode 100755 scripts/apply-rsLXMF-file-attachments-list.sh create mode 100755 scripts/apply-rsReticulum-reply-file-query-metadata.sh delete mode 100644 scripts/ratspeak-stack-ci-pins.env diff --git a/.github/workflows/flatpak.yaml b/.github/workflows/flatpak.yaml index 90634b7f7..33f2a10b8 100644 --- a/.github/workflows/flatpak.yaml +++ b/.github/workflows/flatpak.yaml @@ -67,10 +67,6 @@ jobs: - name: Clone Ratspeak stack (rsReticulum, rsLXMF) env: WORKSPACE_ROOT: ${{ github.workspace }}/.rsstack - # Keep in sync with reticulum-sidecar.yaml / ratspeak-stack-ci-pins.env. - RS_RETICULUM_REF: e16bd152256a5caffb704446bbe15530c1b20f48 - RS_LXMF_REF: c3d8b44942e7726dbbe6bb53e0976d4c72134119 - RS_NOMAD_REF: ec6b6dde23addf9d8248b794238baaa6498fe171 run: bash scripts/clone-ratspeak-stack.sh - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 diff --git a/.github/workflows/reticulum-sidecar.yaml b/.github/workflows/reticulum-sidecar.yaml index 4a35975d9..bd7e916bc 100644 --- a/.github/workflows/reticulum-sidecar.yaml +++ b/.github/workflows/reticulum-sidecar.yaml @@ -3,14 +3,8 @@ name: Reticulum sidecar permissions: contents: read -# Stacked Nomad file/metadata work: pin siblings until upstream PRs merge, then float again. -# ratspeak/rsReticulum#26, ratspeak/rsLXMF#7 (rsNomad#7 merged — float). Keep in sync with -# scripts/ratspeak-stack-ci-pins.env and RATSPEAK_STACK_PR_ENTRIES in scripts/update.sh. -env: - RS_RETICULUM_REF: e16bd152256a5caffb704446bbe15530c1b20f48 - RS_LXMF_REF: c3d8b44942e7726dbbe6bb53e0976d4c72134119 - RS_NOMAD_REF: ec6b6dde23addf9d8248b794238baaa6498fe171 - +# Float rsReticulum / rsLXMF / rsNomad to origin/main; open feature PRs are overlays +# (see reticulum-sidecar/patches/README.md — ReplyFile #26, LXMF attachments #7). on: workflow_dispatch: push: diff --git a/AGENTS.md b/AGENTS.md index 22cf777c0..50f8bca3f 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) and warns when a local overlay can be removed. Stacked **feature** PRs that mesh-client CI pins (not overlays) live in `RATSPEAK_STACK_PR_ENTRIES` + `scripts/ratspeak-stack-ci-pins.env` (`check_ratspeak_stack_prs`) — today [rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) and [rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7); clear pins when those merge. It also runs `check_ratspeak_upstream` (watched **published** releases for rsLXST / lrgp-rs / Ratspeak vs `reviewed-ref` pins, plus new `ratspeak` org repos) — keep `RATSPEAK_RELEASE_WATCH_ENTRIES` / `RATSPEAK_KNOWN_ORG_REPOS` in sync when adopting libs. LXMFace is not a published-release watch: its baseline is a vendored-file commit (`file:js/lxmface.js@`) compared with the latest GitHub commit that touched that file. `scripts/clone-ratspeak-stack.sh` floats **rsReticulum** / **rsLXMF** / **rsNomad** / **rsLXST** / **lrgp-rs** to `origin/main` (override with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF`); overlays must apply or the clone fails. Ratspeak release watch uses stub-kind `games-parity` to nudge Games tab review when a published release is newer than the pin (`docs/reticulum-games-parity.md`). 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). +> **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. Open upstream feature work (e.g. [rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) ReplyFile, [rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) multi-file attachments) is carried as overlays on floated `origin/main` — never as committed SHA pins. It also runs `check_ratspeak_upstream` (watched **published** releases for rsLXST / lrgp-rs / Ratspeak vs `reviewed-ref` pins, plus new `ratspeak` org repos) — keep `RATSPEAK_RELEASE_WATCH_ENTRIES` / `RATSPEAK_KNOWN_ORG_REPOS` in sync when adopting libs. LXMFace is not a published-release watch: its baseline is a vendored-file commit (`file:js/lxmface.js@`) compared with the latest GitHub commit that touched that file. `scripts/clone-ratspeak-stack.sh` floats **rsReticulum** / **rsLXMF** / **rsNomad** / **rsLXST** / **lrgp-rs** to `origin/main` (override with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF` for bisect only); overlays must apply or the clone fails. Ratspeak release watch uses stub-kind `games-parity` to nudge Games tab review when a published release is newer than the pin (`docs/reticulum-games-parity.md`). 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 fce1bac17..a55e82648 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -90,7 +90,7 @@ 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 and local **dev** clones float the `.rsstack/` workspace via `scripts/clone-ratspeak-stack.sh` to `origin/main` (overlays must apply; override with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF` for bisect). When CI must build against open stacked ratspeak PRs, `scripts/ratspeak-stack-ci-pins.env` supplies defaults under `CI=true` (tracked by `RATSPEAK_STACK_PR_ENTRIES` in `scripts/update.sh`; see [reticulum-sidecar/patches/README.md](../reticulum-sidecar/patches/README.md#stacked-upstream-feature-prs-ci-pins)). **Release** packaging (`scripts/build-reticulum-sidecar-release.mjs`) runs the same clone and records the resolved commit SHAs for all five crates in `.rsstack/RESOLVED_SHAS.txt` so artifacts retain the exact source revisions used — pin via `RS_*_REF` when a release must not float. +CI and local **dev** clones float the `.rsstack/` workspace via `scripts/clone-ratspeak-stack.sh` to `origin/main` (overlays must apply; optional `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF` for bisect only — CI never pins Ratspeak SHAs). Open upstream feature PRs needed before they land on `main` (e.g. [rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) ReplyFile, [rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) multi-file attachments) are carried as overlays under `reticulum-sidecar/patches/` and tracked by `RATSPEAK_PATCH_ENTRIES` in `scripts/update.sh`. **Release** packaging (`scripts/build-reticulum-sidecar-release.mjs`) runs the same clone and records the resolved commit SHAs for all five crates in `.rsstack/RESOLVED_SHAS.txt` so artifacts retain the exact source revisions used — set `RS_*_REF` only when a release must not float. 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). @@ -185,14 +185,14 @@ 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, Ratspeak overlay PR checks, stacked feature-PR pin watches - ([rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) ReplyFile, - [rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) multi-file attachments — see - `scripts/ratspeak-stack-ci-pins.env`), and an upstream release / new-org-repo watch - (rsLXST, lrgp-rs, Ratspeak Games-parity when a newer published release exists, LXMFace - `js/lxmface.js` commit). Sibling **rsReticulum** / + also runs dedupe, Ratspeak overlay PR checks + ([rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) ReplyFile and + [rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) multi-file attachments are overlays + on floated `origin/main` — see `reticulum-sidecar/patches/README.md`), and an upstream + release / new-org-repo watch (rsLXST, lrgp-rs, Ratspeak Games-parity when a newer published + release exists, LXMFace `js/lxmface.js` commit). Sibling **rsReticulum** / **rsLXMF** / **rsNomad** / **rsLXST** / **lrgp-rs** float to `origin/main` via - `clone-ratspeak-stack.sh` (overlays must apply; CI may pin open stacked PRs). See AGENTS.md §6. + `clone-ratspeak-stack.sh` (overlays must apply; no committed SHA pins). See AGENTS.md §6. ### Testing Dependabot PRs locally diff --git a/docs/development-environment.md b/docs/development-environment.md index de0e9db34..b769413f2 100644 --- a/docs/development-environment.md +++ b/docs/development-environment.md @@ -119,7 +119,7 @@ pnpm run reticulum:sidecar:build This writes `reticulum-sidecar/target/debug/mesh-client-reticulum` (macOS/Linux) or `.exe` on Windows. -**First-time / recover the stack workspace:** from the mesh-client repo root, run `./scripts/clone-ratspeak-stack.sh`. That script clones (or updates) the repo-local `.rsstack/` workspace checkouts `rsReticulum`, `rsLXMF`, `rsNomad`, `rsLXST`, and `lrgp-rs`, 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` / `RS_LXST_REF` / `RS_LRGP_REF` to a SHA or ref before running the clone script. +**First-time / recover the stack workspace:** from the mesh-client repo root, run `./scripts/clone-ratspeak-stack.sh`. That script clones (or updates) the repo-local `.rsstack/` workspace checkouts `rsReticulum`, `rsLXMF`, `rsNomad`, `rsLXST`, and `lrgp-rs`, floats each to **`origin/main`** by default, and applies mesh-client overlays (fails if a patch will not apply). For bisect only, set `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF` to a SHA or ref before running the clone script — CI and normal updates never pin Ratspeak SHAs. When those `.rsstack/` checkouts 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. diff --git a/docs/reticulum.md b/docs/reticulum.md index edb3cbfb4..8e59cb0aa 100644 --- a/docs/reticulum.md +++ b/docs/reticulum.md @@ -492,7 +492,7 @@ Firmware `.zip` files are selected locally (no in-app GitHub download). Disconne ## Building the sidecar (development) -`rns-stack` builds need the repo-local `.rsstack/` workspace checkouts `rsReticulum`, `rsLXMF`, `rsNomad`, `rsLXST`, and `lrgp-rs` (see `scripts/clone-ratspeak-stack.sh`). That script floats each to `origin/main` by default (bisect with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF`) and applies mesh-client overlays for rsReticulum/rsLXMF (fails if a patch will not apply). CI may pin open stacked feature PRs ([rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26), [rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7)) via `scripts/ratspeak-stack-ci-pins.env` — tracked by `pnpm run update`. 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. +`rns-stack` builds need the repo-local `.rsstack/` workspace checkouts `rsReticulum`, `rsLXMF`, `rsNomad`, `rsLXST`, and `lrgp-rs` (see `scripts/clone-ratspeak-stack.sh`). That script floats each to `origin/main` by default (optional bisect with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF`) and applies mesh-client overlays for rsReticulum/rsLXMF (fails if a patch will not apply). Open upstream feature PRs such as [rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) and [rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) are carried as overlays (not SHA pins) — tracked by `pnpm run update`. 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/reticulum-sidecar/README.md b/reticulum-sidecar/README.md index 6f47cd641..05b3459fa 100644 --- a/reticulum-sidecar/README.md +++ b/reticulum-sidecar/README.md @@ -14,7 +14,7 @@ Install Rust (**1.85+**, edition 2024). Prefer [rustup](https://rustup.rs/). See ./scripts/clone-ratspeak-stack.sh ``` -That floats `rsReticulum` / `rsLXMF` / `rsNomad` / `rsLXST` / `lrgp-rs` under `.rsstack/` to `origin/main` (override with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF` for bisect). CI may temporarily pin rsReticulum / rsLXMF to open stacked PRs via `scripts/ratspeak-stack-ci-pins.env` (see [patches/README.md](patches/README.md#stacked-upstream-feature-prs-ci-pins); tracked by `pnpm run update`). Peer default avatars use [LXMFace](https://github.com/ratspeak/LXMFace) in the **renderer** (`src/renderer/lib/reticulum/lxmface.ts`), not this sidecar. +That floats `rsReticulum` / `rsLXMF` / `rsNomad` / `rsLXST` / `lrgp-rs` under `.rsstack/` to `origin/main` (optional `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF` for bisect only). Open upstream feature PRs needed before they land on `main` (e.g. ReplyFile, multi-file LXMF attachments) are applied as overlays — see [patches/README.md](patches/README.md). 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 the `.rsstack/` checkouts on disk (CI runs `clone-ratspeak-stack.sh`; locally use the script above): @@ -87,7 +87,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). -- **Ratspeak / Nomad / LXST / LRGP siblings:** `scripts/clone-ratspeak-stack.sh` floats `rsReticulum` / `rsLXMF` / `rsNomad` / `rsLXST` / `lrgp-rs` to `origin/main` (override with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF`); overlays must apply for rsReticulum/rsLXMF. +- **Ratspeak / Nomad / LXST / LRGP siblings:** `scripts/clone-ratspeak-stack.sh` floats `rsReticulum` / `rsLXMF` / `rsNomad` / `rsLXST` / `lrgp-rs` to `origin/main` (optional `RS_*_REF` for bisect only); overlays must apply for rsReticulum/rsLXMF. ## API diff --git a/reticulum-sidecar/patches/README.md b/reticulum-sidecar/patches/README.md index 2f38f0c0e..c60e9224d 100644 --- a/reticulum-sidecar/patches/README.md +++ b/reticulum-sidecar/patches/README.md @@ -2,26 +2,17 @@ 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 (`.rsstack/rsNomad` from [Colorado-Mesh/rsNomad](https://github.com/Colorado-Mesh/rsNomad) is also required for Nomad hosting; no mesh-client overlay today). Checkouts live in the repo-local `.rsstack/` gitignored workspace, keeping a standalone `rsReticulum` mirror (if present) pristine. -By default `scripts/clone-ratspeak-stack.sh` floats the `.rsstack/` checkouts 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. +By default `scripts/clone-ratspeak-stack.sh` floats the `.rsstack/` checkouts 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` only as a manual bisect escape hatch — CI and `pnpm run update` never pin Ratspeak SHAs. 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. -## Stacked upstream feature PRs (CI pins) - -mesh-client sometimes depends on **open** ratspeak PRs that are not local overlays (new library APIs). Those are pinned for CI via `scripts/ratspeak-stack-ci-pins.env` (loaded when `CI=true`) and matching workflow `env` blocks. `pnpm run update` tracks them in `RATSPEAK_STACK_PR_ENTRIES` (`scripts/update.sh`) and warns when they merge so pins can be cleared. - -| Upstream | What we need | Pin / watch | -| -------- | ------------ | ----------- | -| [ratspeak/rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) | `RequestOutcome::ReplyFile` + `LinkClient::query` Resource metadata | `RS_RETICULUM_REF` | -| [ratspeak/rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) | Multi-file LXMF attachment pack/list APIs | `RS_LXMF_REF` | - -After both merge and floated `origin/main` includes them: delete or empty `ratspeak-stack-ci-pins.env`, drop workflow env pins, and remove the matching `RATSPEAK_STACK_PR_ENTRIES` rows. +Open upstream feature PRs that mesh-client needs before they land on `main` (for example [rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) ReplyFile, [rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) multi-file attachments) are carried as **overlays** below — same apply path as other patches. `pnpm run update` tracks them in `RATSPEAK_PATCH_ENTRIES` and warns when the upstream PR merges so the overlay can be removed. Colorado-Mesh/rsNomad floats `origin/main` with **no** mesh-client overlay (NomadNet `/media` is already on main). ## Development — overlays/patches Overlays require **git checkouts** in the repo-local `.rsstack/` workspace (not a bare Cargo cache path): -- `.rsstack/rsReticulum` — floated to `origin/main` unless `RS_RETICULUM_REF` is set -- `.rsstack/rsLXMF` — floated to `origin/main` unless `RS_LXMF_REF` is set -- `.rsstack/rsNomad` — floated to `origin/main` unless `RS_NOMAD_REF` is set +- `.rsstack/rsReticulum` — floated to `origin/main` unless `RS_RETICULUM_REF` is set (bisect only) +- `.rsstack/rsLXMF` — floated to `origin/main` unless `RS_LXMF_REF` is set (bisect only) +- `.rsstack/rsNomad` — floated to `origin/main` unless `RS_NOMAD_REF` is set (bisect only) **First-time setup:** @@ -41,6 +32,43 @@ git -C .rsstack/rsReticulum status --short If a patch is skipped or conflicts after an upstream bump, CI/`ensure-rsReticulum-patches.sh` will fail. Rebase the overlay, regenerate the `.patch` file per the section below, then re-run the apply script. +## rsReticulum-reply-file-query-metadata.patch + +Carry [ratspeak/rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) on floated `origin/main`: `RequestOutcome::ReplyFile`, `pack_file_name_metadata`, 4-arg `set_request_handler_ex` (remote identity), and `LinkClient::query` Resource metadata. Required for Colorado-Mesh/rsNomad NomadNet `/file` and `/media` response Resources. + +| Field | Value | +| ----- | ----- | +| **Base commit** | `9bc7ee5ff9caf04cfb3ba5a50bd19394aeaf9d33` (`ratspeak/rsReticulum` `origin/main`) | +| **Upstream PR** | https://github.com/ratspeak/rsReticulum/pull/26 | + +**Adds (8 files):** + +- `crates/rns-runtime/src/link_manager.rs` — `ReplyFile`, `pack_file_name_metadata`, handler remote identity +- `crates/rns-runtime/src/link_client.rs` — query metadata / `LinkQueryResponse` +- `crates/rns-runtime/src/{lib,reticulum,rncp}.rs` — re-exports and call sites +- `crates/rns-tools/src/commands/{rnpath,rnstatus}.rs` — tool updates +- `api/snapshots/rns-runtime.txt` — API snapshot + +### Apply locally + +```bash +./scripts/apply-rsReticulum-reply-file-query-metadata.sh +``` + +### Regenerate + +```bash +# From a clean floated main checkout matching Base commit (or newer tip): +gh pr diff 26 --repo ratspeak/rsReticulum \ + > reticulum-sidecar/patches/rsReticulum-reply-file-query-metadata.patch +git -C .rsstack/rsReticulum apply --check \ + "$(pwd)/reticulum-sidecar/patches/rsReticulum-reply-file-query-metadata.patch" +``` + +### Sunset + +When [ratspeak/rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) merges and floated `origin/main` includes it, remove this patch, `scripts/apply-rsReticulum-reply-file-query-metadata.sh`, the apply-list entry, and the `RATSPEAK_PATCH_ENTRIES` row. + ## rsReticulum-packet-tap.patch Wire packet tap API for the Reticulum Stats/Sniffer panel (`wire_packet` WebSocket events, `GET /api/v1/packets`). @@ -279,6 +307,39 @@ git diff -- \ 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-file-attachments-list.patch + +Carry [ratspeak/rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) on floated `origin/main`: multi-file `set_file_attachments_field` / `file_attachments` (single-file helper remains). + +| Field | Value | +| ----- | ----- | +| **Base commit** | `e609864d8898d70d0a67072e527aa012055edcab` (`ratspeak/rsLXMF` `origin/main`) | +| **Upstream PR** | https://github.com/ratspeak/rsLXMF/pull/7 | + +**Adds (2 files):** + +- `crates/lxmf-core/src/message.rs` — multi-file attachment pack/list APIs + tests +- `README.md` — field table for typed media setters + +### Apply locally + +```bash +./scripts/apply-rsLXMF-file-attachments-list.sh +``` + +### Regenerate + +```bash +gh pr diff 7 --repo ratspeak/rsLXMF \ + > reticulum-sidecar/patches/rsLXMF-file-attachments-list.patch +git -C .rsstack/rsLXMF apply --check \ + "$(pwd)/reticulum-sidecar/patches/rsLXMF-file-attachments-list.patch" +``` + +### Sunset + +When [ratspeak/rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) merges and floated `origin/main` includes it, remove this patch, `scripts/apply-rsLXMF-file-attachments-list.sh`, the apply-list entry, and the `RATSPEAK_PATCH_ENTRIES` row. + ## rsLXMF-propagation-sync-peering.patch LinkIdentify + peering stamp before LXMF `/offer`, sticky offer/finish fields, plus Establishing diagnostics (`last_establish_error` + warn when LRPROOF is ignored for missing identity or invalid proof) so mesh-client can complete remote PN sync and surface non-generic failures. diff --git a/reticulum-sidecar/patches/rsLXMF-file-attachments-list.patch b/reticulum-sidecar/patches/rsLXMF-file-attachments-list.patch new file mode 100644 index 000000000..51484c003 --- /dev/null +++ b/reticulum-sidecar/patches/rsLXMF-file-attachments-list.patch @@ -0,0 +1,181 @@ +diff --git a/README.md b/README.md +index c708d18..f549b5c 100644 +--- a/README.md ++++ b/README.md +@@ -312,10 +312,18 @@ signature 64 bytes + payload MessagePack([timestamp, title, content, fields, optional_stamp]) + ``` + +-`title` and `content` are bytes on the wire. `fields` is a `map` for ++`title` and `content` are bytes on the wire. `fields` is a `map` for + application-defined data such as tickets, attachments, location data, or + application envelopes. + ++Native media fields (use typed setters — do not `set_field` raw bins for these): ++ ++| Field | Wire shape | API | ++| --- | --- | --- | ++| `FIELD_IMAGE` (`0x06`) | `[format, bytes]` | `set_image_field` / `image_attachment` | ++| `FIELD_FILE_ATTACHMENTS` (`0x05`) | `[[name, bytes], …]` | `set_file_attachments_field` / `file_attachments` (single-file helper: `set_file_attachment_field`) | ++| `FIELD_AUDIO` (`0x07`) | `[mode, bytes]` | `set_audio_field` / `audio_field` | ++ + Library callers requesting a reply ticket set `include_ticket`, call + `LxmRouter::prepare_outbound`, and then sign the message. The router rejects a + requested ticket that would require changing an already-signed message. +diff --git a/crates/lxmf-core/src/message.rs b/crates/lxmf-core/src/message.rs +index efbe123..b914362 100644 +--- a/crates/lxmf-core/src/message.rs ++++ b/crates/lxmf-core/src/message.rs +@@ -316,11 +316,19 @@ impl LxMessage { + file_name: &str, + file_bytes: &[u8], + ) -> Result<(), MessageError> { +- let encoded = rmp_serde::to_vec(&FileAttachmentsFieldRef { +- file_name, +- bytes: file_bytes, +- }) +- .map_err(|error| MessageError::PackFailed(error.to_string()))?; ++ self.set_file_attachments_field(&[(file_name, file_bytes)]) ++ } ++ ++ /// Encode and install native LXMF file attachments (`[[name, bytes], ...]`). ++ /// ++ /// Matches Python NomadNet / LXMF multi-file attachment lists. Replaces any ++ /// previous `FIELD_FILE_ATTACHMENTS` value. ++ pub fn set_file_attachments_field( ++ &mut self, ++ attachments: &[(&str, &[u8])], ++ ) -> Result<(), MessageError> { ++ let encoded = rmp_serde::to_vec(&FileAttachmentsListRef { attachments }) ++ .map_err(|error| MessageError::PackFailed(error.to_string()))?; + self.fields.insert(FIELD_FILE_ATTACHMENTS, encoded); + self.msgpack_field_ids.insert(FIELD_FILE_ATTACHMENTS); + Ok(()) +@@ -333,22 +341,30 @@ impl LxMessage { + /// Decode the first native LXMF file attachment while borrowing its bytes + /// from this message. Only the small filename is allocated. + pub fn first_file_attachment(&self) -> Result, MessageError> { ++ Ok(self.file_attachments()?.into_iter().next()) ++ } ++ ++ /// Decode all native LXMF file attachments, borrowing each payload from ++ /// this message. Filenames are allocated; bytes are not. ++ pub fn file_attachments(&self) -> Result, MessageError> { + let Some(field) = self.fields.get(&FIELD_FILE_ATTACHMENTS) else { +- return Ok(None); ++ return Ok(Vec::new()); + }; + let mut input = field.as_slice(); +- if read_msgpack_array_len(&mut input)? == 0 { +- return Ok(None); +- } +- if read_msgpack_array_len(&mut input)? < 2 { +- return Err(MessageError::UnpackFailed( +- "file attachment entry requires name and data".to_string(), +- )); ++ let count = read_msgpack_array_len(&mut input)? as usize; ++ let mut out = Vec::with_capacity(count); ++ for _ in 0..count { ++ if read_msgpack_array_len(&mut input)? < 2 { ++ return Err(MessageError::UnpackFailed( ++ "file attachment entry requires name and data".to_string(), ++ )); ++ } ++ let file_name = ++ String::from_utf8_lossy(read_msgpack_string_or_binary(&mut input)?).into_owned(); ++ let data = read_msgpack_binary(&mut input)?; ++ out.push((file_name, data)); + } +- let file_name = +- String::from_utf8_lossy(read_msgpack_string_or_binary(&mut input)?).into_owned(); +- let data = read_msgpack_binary(&mut input)?; +- Ok(Some((file_name, data))) ++ Ok(out) + } + + /// Decode the native LXMF image field while borrowing its image bytes +@@ -1742,12 +1758,11 @@ impl serde::Serialize for AudioFieldRef<'_> { + } + } + +-struct FileAttachmentsFieldRef<'a> { +- file_name: &'a str, +- bytes: &'a [u8], ++struct FileAttachmentsListRef<'a> { ++ attachments: &'a [(&'a str, &'a [u8])], + } + +-impl serde::Serialize for FileAttachmentsFieldRef<'_> { ++impl serde::Serialize for FileAttachmentsListRef<'_> { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::{SerializeSeq, SerializeTuple}; + +@@ -1765,11 +1780,13 @@ impl serde::Serialize for FileAttachmentsFieldRef<'_> { + } + } + +- let mut attachments = serializer.serialize_seq(Some(1))?; +- attachments.serialize_element(&AttachmentRef { +- file_name: self.file_name, +- bytes: self.bytes, +- })?; ++ let mut attachments = serializer.serialize_seq(Some(self.attachments.len()))?; ++ for (file_name, bytes) in self.attachments { ++ attachments.serialize_element(&AttachmentRef { ++ file_name, ++ bytes, ++ })?; ++ } + attachments.end() + } + } +@@ -2451,6 +2468,49 @@ mod tests { + assert!(msg.msgpack_field_ids.contains(&FIELD_FILE_ATTACHMENTS)); + } + ++ #[test] ++ fn typed_multi_file_attachments_round_trip() { ++ let a = b"one"; ++ let b = b"two"; ++ let expected_value = rmpv::Value::Array(vec![ ++ rmpv::Value::Array(vec![ ++ rmpv::Value::String("a.txt".into()), ++ rmpv::Value::Binary(a.to_vec()), ++ ]), ++ rmpv::Value::Array(vec![ ++ rmpv::Value::String("b.bin".into()), ++ rmpv::Value::Binary(b.to_vec()), ++ ]), ++ ]); ++ let mut expected = Vec::new(); ++ rmpv::encode::write_value(&mut expected, &expected_value).unwrap(); ++ ++ let mut msg = LxMessage::new([0; 16], [0; 16], "", "", DeliveryMethod::Direct); ++ msg.set_file_attachments_field(&[("a.txt", a.as_slice()), ("b.bin", b.as_slice())]) ++ .unwrap(); ++ assert_eq!(msg.get_field(FIELD_FILE_ATTACHMENTS), Some(&expected)); ++ ++ let listed = msg.file_attachments().unwrap(); ++ assert_eq!(listed.len(), 2); ++ assert_eq!(listed[0].0, "a.txt"); ++ assert_eq!(listed[0].1, a); ++ assert_eq!(listed[1].0, "b.bin"); ++ assert_eq!(listed[1].1, b); ++ assert_eq!( ++ msg.first_file_attachment().unwrap().unwrap().0, ++ "a.txt" ++ ); ++ ++ let key = rns_crypto::ed25519::Ed25519PrivateKey::generate(); ++ msg.sign(&key).unwrap(); ++ let packed = msg.pack().unwrap(); ++ let inbound = LxMessage::unpack(&packed).unwrap(); ++ let inbound_files = inbound.file_attachments().unwrap(); ++ assert_eq!(inbound_files.len(), 2); ++ assert_eq!(inbound_files[1].0, "b.bin"); ++ assert_eq!(inbound_files[1].1, b); ++ } ++ + #[test] + fn native_attachment_decoders_borrow_large_payload_bytes() { + let file_bytes = vec![0x5a; 2 * 1024 * 1024]; diff --git a/reticulum-sidecar/patches/rsReticulum-reply-file-query-metadata.patch b/reticulum-sidecar/patches/rsReticulum-reply-file-query-metadata.patch new file mode 100644 index 000000000..4547e2b3a --- /dev/null +++ b/reticulum-sidecar/patches/rsReticulum-reply-file-query-metadata.patch @@ -0,0 +1,504 @@ +diff --git a/api/snapshots/rns-runtime.txt b/api/snapshots/rns-runtime.txt +index b7fca02..f1ededf 100644 +--- a/api/snapshots/rns-runtime.txt ++++ b/api/snapshots/rns-runtime.txt +@@ -587,7 +587,7 @@ pub fn rns_runtime::link_manager::LinkManager::set_link_packet_channel(&mut self + pub fn rns_runtime::link_manager::LinkManager::set_link_packet_proof_channel(&mut self, tokio::sync::mpsc::bounded::Sender) + pub fn rns_runtime::link_manager::LinkManager::set_outbound_resource_proof_channel(&mut self, tokio::sync::mpsc::bounded::Sender) + pub fn rns_runtime::link_manager::LinkManager::set_request_handler(&mut self, F) where F: core::ops::function::Fn([u8; 16], [u8; 16], alloc::vec::Vec) -> core::option::Option> + core::marker::Send + 'static +-pub fn rns_runtime::link_manager::LinkManager::set_request_handler_ex(&mut self, F) where F: core::ops::function::Fn([u8; 16], [u8; 16], alloc::vec::Vec) -> rns_runtime::link_manager::RequestOutcome + core::marker::Send + 'static ++pub fn rns_runtime::link_manager::LinkManager::set_request_handler_ex(&mut self, F) where F: core::ops::function::Fn([u8; 16], [u8; 16], alloc::vec::Vec, core::option::Option) -> rns_runtime::link_manager::RequestOutcome + core::marker::Send + 'static + pub fn rns_runtime::link_manager::LinkManager::set_resource_accept_handler(&mut self, F) where F: core::ops::function::Fn([u8; 16], &rns_protocol::resource_adv::ResourceAdvertisement) -> bool + core::marker::Send + 'static + pub fn rns_runtime::link_manager::LinkManager::set_resource_completed_channel(&mut self, tokio::sync::mpsc::bounded::Sender<(alloc::vec::Vec, [u8; 16])>) + pub fn rns_runtime::link_manager::LinkManager::set_resource_completion_channel(&mut self, tokio::sync::mpsc::bounded::Sender) +diff --git a/crates/rns-runtime/src/lib.rs b/crates/rns-runtime/src/lib.rs +index a67c07a..47ffadb 100644 +--- a/crates/rns-runtime/src/lib.rs ++++ b/crates/rns-runtime/src/lib.rs +@@ -39,7 +39,9 @@ pub mod prelude { + RegisteredDestination, ResourceAcceptPolicy, + }; + pub use crate::lifecycle::ShutdownSignal; +- pub use crate::link_manager::{DestinationAnnounceOptions, DestinationRequest, RequestOutcome}; ++ pub use crate::link_manager::{ ++ DestinationAnnounceOptions, DestinationRequest, RequestOutcome, pack_file_name_metadata, ++ }; + pub use crate::link_session::{ + LinkSession, LinkSessionChannelError, LinkSessionChannelHandle, LinkSessionCloseReason, + LinkSessionError, LinkSessionEvent, LinkSessionHandle, LinkSessionResourceError, +diff --git a/crates/rns-runtime/src/link_client.rs b/crates/rns-runtime/src/link_client.rs +index 4f48bc8..fce828f 100644 +--- a/crates/rns-runtime/src/link_client.rs ++++ b/crates/rns-runtime/src/link_client.rs +@@ -45,6 +45,17 @@ pub enum LinkClientError { + UnexpectedResponse(String), + } + ++/// Successful Link request response. ++/// ++/// Ordinary replies carry packed response bytes in [`Self::data`] with ++/// [`Self::metadata`] unset. NomadNet `/file/...` replies are response Resources ++/// whose payload is raw file bytes plus optional msgpack filename metadata. ++#[derive(Debug, Clone, PartialEq, Eq)] ++pub struct LinkQueryResponse { ++ pub data: Vec, ++ pub metadata: Option>, ++} ++ + #[derive(Clone)] + pub struct LinkClient { + transport_tx: mpsc::Sender, +@@ -66,7 +77,7 @@ impl LinkClient { + } + + /// Open a Link to `app_name` on `remote_transport_hash`, send one +- /// request, return the response. ++ /// request, return the response (and optional Resource metadata). + pub async fn query( + &self, + remote_transport_hash: [u8; 16], +@@ -75,7 +86,7 @@ impl LinkClient { + payload: Vec, + hops: u8, + overall_timeout: Duration, +- ) -> Result, LinkClientError> { ++ ) -> Result { + let started = Instant::now(); + let deadline = started + overall_timeout; + +@@ -280,7 +291,7 @@ async fn wait_for_response( + link_id: [u8; 16], + request_id: [u8; 16], + deadline: Duration, +-) -> Result, LinkClientError> { ++) -> Result { + let fut = async { + let mut inbound_resources: HashMap<[u8; 32], InboundTransfer> = HashMap::new(); + +@@ -318,7 +329,10 @@ async fn wait_for_response( + match link.handle_response(body) { + Ok((id, response_data)) => { + if id == request_id { +- return Ok(response_data); ++ return Ok(LinkQueryResponse { ++ data: response_data, ++ metadata: None, ++ }); + } + } + Err(e) => { +@@ -432,7 +446,7 @@ async fn wait_for_response( + } + + if let Some(rh) = completed_rh { +- let (assembled, proof) = { ++ let (assembled, proof, metadata) = { + let transfer = + inbound_resources.get_mut(&rh).ok_or_else(|| { + LinkClientError::UnexpectedResponse( +@@ -451,19 +465,32 @@ async fn wait_for_response( + }, + ) + }; +- transfer.complete(Some(&decrypt_fn)).map_err(|e| { +- LinkClientError::UnexpectedResponse(format!( +- "resource assemble: {e:?}" +- )) +- })? ++ let (assembled, proof) = ++ transfer.complete(Some(&decrypt_fn)).map_err(|e| { ++ LinkClientError::UnexpectedResponse(format!( ++ "resource assemble: {e:?}" ++ )) ++ })?; ++ let metadata = transfer.resource.metadata.clone(); ++ (assembled, proof, metadata) + }; + + send_link_proof(transport_tx, link_id, &proof).await?; + inbound_resources.remove(&rh); ++ if metadata.is_some() { ++ // NomadNet file response: raw payload + Resource metadata. ++ return Ok(LinkQueryResponse { ++ data: assembled, ++ metadata, ++ }); ++ } + match link.handle_response_plaintext(&assembled) { + Ok((id, response_data)) => { + if id == request_id { +- return Ok(response_data); ++ return Ok(LinkQueryResponse { ++ data: response_data, ++ metadata: None, ++ }); + } + } + Err(e) => { +diff --git a/crates/rns-runtime/src/link_manager.rs b/crates/rns-runtime/src/link_manager.rs +index 62ec51f..36b8b92 100644 +--- a/crates/rns-runtime/src/link_manager.rs ++++ b/crates/rns-runtime/src/link_manager.rs +@@ -402,6 +402,8 @@ impl std::fmt::Debug for LinkManagerAccountingEvent { + /// Result of an extended request handler. `Reply` is the ordinary response; + /// `ReplyWithResource` sends an inline ack followed by a resource transfer + /// (rncp --fetch). Python: `RNS.Resource(..., target_link=link)`. ++/// `ReplyFile` sends a **response** Resource with raw file bytes and optional ++/// msgpack metadata (NomadNet `/file/...`: `{"name": }`). + #[derive(Debug, Clone)] + pub enum RequestOutcome { + Reply(Vec), +@@ -412,10 +414,32 @@ pub enum RequestOutcome { + metadata: Option>, + auto_compress: bool, + }, ++ /// Response Resource with raw payload (not packed `[request_id, body]`). ++ /// ++ /// Python NomadNet `serve_file` returns `[file_handle, {"name": ...}]`, ++ /// which becomes `RNS.Resource(..., metadata=..., is_response=True)`. ++ ReplyFile { ++ data: Vec, ++ /// Optional msgpack-encoded metadata (e.g. from [`pack_file_name_metadata`]). ++ metadata: Option>, ++ auto_compress: bool, ++ }, + /// Silently drop; caller sees a timeout. Useful for ACL denies. + Drop, + } + ++/// Pack NomadNet / rncp-compatible Resource filename metadata: ++/// msgpack map `{"name": }`. ++pub fn pack_file_name_metadata(file_name: &str) -> Vec { ++ let entries = vec![( ++ rmpv::Value::String(rmpv::Utf8String::from("name")), ++ rmpv::Value::Binary(file_name.as_bytes().to_vec()), ++ )]; ++ let mut buf = Vec::new(); ++ let _ = rmpv::encode::write_value(&mut buf, &rmpv::Value::Map(entries)); ++ buf ++} ++ + /// Python-compatible context supplied to a per-path Destination request handler. + #[derive(Clone)] + pub struct DestinationRequest { +@@ -431,7 +455,10 @@ pub type DestinationRequestHandler = + Box RequestOutcome + Send + 'static>; + + type RequestHandler = Box) -> Option> + Send>; +-type RequestHandlerEx = Box) -> RequestOutcome + Send>; ++/// Extended catch-all handler: link id, path hash, request body, remote identity ++/// (when the peer identified on the Link). ++type RequestHandlerEx = ++ Box, Option) -> RequestOutcome + Send>; + type LinkIdentityGate = Box bool + Send>; + type ResourceAcceptHandler = Box bool + Send>; + +@@ -4675,7 +4702,7 @@ impl LinkManager { + RequestOutcome::Drop + } + } else if let Some(ref handler) = self.request_handler_ex { +- handler(link_id, path_hash, data.clone()) ++ handler(link_id, path_hash, data.clone(), remote_identity) + } else if let Some(ref handler) = self.request_handler { + match handler(link_id, path_hash, data) { + Some(response) => RequestOutcome::Reply(response), +@@ -4685,15 +4712,20 @@ impl LinkManager { + RequestOutcome::Drop + }; + +- let (response, fetch_spec) = match outcome { +- RequestOutcome::Reply(response) => (Some(response), None), ++ let (response, fetch_spec, file_spec) = match outcome { ++ RequestOutcome::Reply(response) => (Some(response), None, None), + RequestOutcome::ReplyWithResource { + ack, + data, + metadata, + auto_compress, +- } => (Some(ack), Some((data, metadata, auto_compress))), +- RequestOutcome::Drop => (None, None), ++ } => (Some(ack), Some((data, metadata, auto_compress)), None), ++ RequestOutcome::ReplyFile { ++ data, ++ metadata, ++ auto_compress, ++ } => (None, None, Some((data, metadata, auto_compress))), ++ RequestOutcome::Drop => (None, None, None), + }; + + if let Some(response) = response { +@@ -4768,7 +4800,7 @@ impl LinkManager { + ); + } + } +- } else { ++ } else if file_spec.is_none() && fetch_spec.is_none() { + tracing::debug!( + link_id = hex::encode(link_id), + request_id = hex::encode(request_id), +@@ -4777,6 +4809,35 @@ impl LinkManager { + ); + } + ++ if let Some((data, metadata, auto_compress)) = file_spec { ++ if self ++ .start_resource_transfer_inner( ++ &link_id, ++ ResourceTransferStart { ++ data, ++ metadata, ++ auto_compress, ++ request_id: Some(request_id.to_vec()), ++ is_response: true, ++ allow_handshake: true, ++ }, ++ ) ++ .is_none() ++ { ++ tracing::warn!( ++ link_id = hex::encode(link_id), ++ request_id = hex::encode(request_id), ++ "link request file response Resource could not be started" ++ ); ++ } else { ++ tracing::debug!( ++ link_id = hex::encode(link_id), ++ request_id = hex::encode(request_id), ++ "link request handled — file response Resource started" ++ ); ++ } ++ } ++ + if let Some((data, metadata, auto_compress)) = fetch_spec { + if self + .start_resource_transfer_inner( +@@ -4809,9 +4870,12 @@ impl LinkManager { + + /// Handler that may schedule a follow-up resource transfer (rncp --fetch). + /// Takes precedence over [`Self::set_request_handler`]. ++ /// ++ /// The fourth argument is the authenticated remote identity when the peer ++ /// identified on the Link (needed for NomadNet `.allowed` ACLs). + pub fn set_request_handler_ex(&mut self, handler: F) + where +- F: Fn([u8; 16], [u8; 16], Vec) -> RequestOutcome + Send + 'static, ++ F: Fn([u8; 16], [u8; 16], Vec, Option) -> RequestOutcome + Send + 'static, + { + self.request_handler_ex = Some(Box::new(handler)); + } +@@ -7162,6 +7226,137 @@ mod tests { + assert_eq!(response_data, b"ready"); + } + ++ #[test] ++ fn pack_file_name_metadata_uses_binary_name() { ++ let packed = pack_file_name_metadata("photos/pic.png"); ++ let value = rmpv::decode::read_value(&mut &packed[..]).unwrap(); ++ let map = value.as_map().expect("metadata map"); ++ assert_eq!(map.len(), 1); ++ assert_eq!(map[0].0.as_str(), Some("name")); ++ assert_eq!(map[0].1.as_slice(), Some(b"photos/pic.png".as_slice())); ++ } ++ ++ #[test] ++ fn reply_file_starts_response_resource_with_filename_metadata() { ++ let dest_hash = [0x42; 16]; ++ let identity_key = Ed25519PrivateKey::generate(); ++ let identity_pub = identity_key.public_key(); ++ let (mut initiator, request_data) = Link::new_initiator(dest_hash, 1); ++ let (responder, proof_data) = ++ Link::new_responder(&request_data, &identity_key, dest_hash, 1).unwrap(); ++ let _rtt_data = initiator ++ .validate_proof(&proof_data, &identity_pub, &identity_pub.to_bytes()) ++ .unwrap(); ++ let link_id = responder.link_id; ++ assert_eq!(responder.state, LinkState::Handshake); ++ ++ let (transport_tx, mut transport_rx) = mpsc::channel(16); ++ let (_event_tx, event_rx) = mpsc::channel(16); ++ let mut manager = LinkManager::new(transport_tx, event_rx, dest_hash, None); ++ manager.active_links.insert( ++ link_id, ++ ActiveLink { ++ link: responder, ++ _interface_id: 1, ++ channel: None, ++ inbound_resources: HashMap::new(), ++ outbound_resources: HashMap::new(), ++ outbound_split_queues: HashMap::new(), ++ inbound_split_resources: HashMap::new(), ++ segment_routing: HashMap::new(), ++ }, ++ ); ++ ++ let file_bytes = b"PNG-BYTES".to_vec(); ++ let metadata = pack_file_name_metadata("photos/pic.png"); ++ assert!(manager.register_request_handler( ++ "/file/photos/pic.png", ++ AllowPolicy::AllowAll, ++ None, ++ true, ++ { ++ let file_bytes = file_bytes.clone(); ++ let metadata = metadata.clone(); ++ move |_| RequestOutcome::ReplyFile { ++ data: file_bytes.clone(), ++ metadata: Some(metadata.clone()), ++ auto_compress: true, ++ } ++ }, ++ )); ++ ++ let (encrypted_request, _) = initiator ++ .request( ++ "/file/photos/pic.png", ++ None, ++ std::time::Duration::from_secs(5), ++ ) ++ .unwrap(); ++ let request_header = rns_wire::header::PacketHeader { ++ flags: rns_wire::flags::PacketFlags { ++ header_type: rns_wire::flags::HeaderType::Header1, ++ context_flag: false, ++ transport_type: rns_wire::flags::TransportType::Broadcast, ++ destination_type: rns_wire::flags::DestinationType::Link, ++ packet_type: rns_wire::flags::PacketType::Data, ++ }, ++ hops: 0, ++ transport_id: None, ++ destination_hash: link_id, ++ context: rns_wire::context::PacketContext::Request, ++ }; ++ let mut raw = request_header.pack(); ++ raw.extend_from_slice(&encrypted_request); ++ ++ manager.handle_inbound_packet(&raw, 1); ++ ++ let active = manager.active_links.get(&link_id).unwrap(); ++ assert_eq!( ++ active.outbound_resources.len(), ++ 1, ++ "ReplyFile must start one outbound response Resource" ++ ); ++ let transfer = active.outbound_resources.values().next().unwrap(); ++ assert!(transfer.resource.flags.is_response); ++ assert!(transfer.resource.flags.has_metadata); ++ let stored = transfer.resource.metadata.as_deref().expect("metadata"); ++ // OutboundResource frames metadata as `length(3 BE) || msgpack`. ++ assert!( ++ stored ++ .windows(metadata.len()) ++ .any(|w| w == metadata.as_slice()), ++ "stored metadata should contain packed name map" ++ ); ++ assert_eq!( ++ transfer.resource.request_id.as_deref(), ++ Some( ++ rns_wire::hash::truncated_packet_hash(&raw, request_header.flags.header_type) ++ .as_slice() ++ ) ++ ); ++ ++ let TransportMessage::Outbound(adv_msg) = ++ next_transport_message(&mut transport_rx).expect("resource advertisement") ++ else { ++ panic!("expected outbound advertisement"); ++ }; ++ let (adv_header, _) = rns_wire::header::PacketHeader::unpack(&adv_msg.raw).unwrap(); ++ assert_eq!( ++ adv_header.context, ++ rns_wire::context::PacketContext::ResourceAdv ++ ); ++ // Drain any follow-up Resource parts; none should be an inline RESPONSE. ++ while let Ok(TransportMessage::Outbound(msg)) = next_transport_message(&mut transport_rx) { ++ let (header, _) = rns_wire::header::PacketHeader::unpack(&msg.raw).unwrap(); ++ assert_ne!( ++ header.context, ++ rns_wire::context::PacketContext::Response, ++ "ReplyFile must not emit a packed inline response" ++ ); ++ } ++ let _ = file_bytes; ++ } ++ + #[test] + fn test_destination_link_acceptance_gating() { + let (tx, _transport_rx) = mpsc::channel(64); +diff --git a/crates/rns-runtime/src/reticulum.rs b/crates/rns-runtime/src/reticulum.rs +index 121e565..171c9da 100644 +--- a/crates/rns-runtime/src/reticulum.rs ++++ b/crates/rns-runtime/src/reticulum.rs +@@ -6189,7 +6189,9 @@ async fn start_blackhole_subscriber(handle: ReticulumHandle) { + { + Ok(payload) => { + match handle +- .query_transport(TransportQuery::ApplyBlackholeManifest { payload }) ++ .query_transport(TransportQuery::ApplyBlackholeManifest { ++ payload: payload.data, ++ }) + .await + { + Some(TransportQueryResponse::IntResult(applied)) => { +diff --git a/crates/rns-runtime/src/rncp.rs b/crates/rns-runtime/src/rncp.rs +index be76a7b..041b146 100644 +--- a/crates/rns-runtime/src/rncp.rs ++++ b/crates/rns-runtime/src/rncp.rs +@@ -279,7 +279,7 @@ pub async fn spawn_rncp_listener( + let link_identities = link_mgr.link_identities_handle(); + let fetch_events = events_tx.clone(); + let fetch_path_hash = truncated_hash(FETCH_PATH_NAME.as_bytes()); +- link_mgr.set_request_handler_ex(move |link_id, path_hash, data| { ++ link_mgr.set_request_handler_ex(move |link_id, path_hash, data, _remote_identity| { + if path_hash != fetch_path_hash { + return RequestOutcome::Drop; + } +@@ -683,13 +683,7 @@ pub async fn rncp_send_file(request: RncpSendRequest<'_>) -> Result Vec { +- let entries = vec![( +- rmpv::Value::String(rmpv::Utf8String::from("name")), +- rmpv::Value::Binary(file_name.as_bytes().to_vec()), +- )]; +- let mut buf = Vec::new(); +- let _ = rmpv::encode::write_value(&mut buf, &rmpv::Value::Map(entries)); +- buf ++ crate::link_manager::pack_file_name_metadata(file_name) + } + + struct OutboundDrive<'a> { +diff --git a/crates/rns-tools/src/commands/rnpath.rs b/crates/rns-tools/src/commands/rnpath.rs +index 22eea9a..663fea3 100644 +--- a/crates/rns-tools/src/commands/rnpath.rs ++++ b/crates/rns-tools/src/commands/rnpath.rs +@@ -951,7 +951,7 @@ async fn run_remote_blackhole_list(args: Args) -> ExitCode { + ) + .await + { +- Ok(b) => b, ++ Ok(b) => b.data, + Err(e) => { + eprintln!( + "rnpath-rs: remote blackhole query failed: {}", +@@ -1183,7 +1183,7 @@ async fn run_remote(args: Args) -> ExitCode { + ) + .await + { +- Ok(b) => b, ++ Ok(b) => b.data, + Err(e) => { + eprintln!("rnpath-rs: remote query failed: {}", remote_err(&e)); + shutdown.trigger(); +diff --git a/crates/rns-tools/src/commands/rnstatus.rs b/crates/rns-tools/src/commands/rnstatus.rs +index a79fac5..2a41259 100644 +--- a/crates/rns-tools/src/commands/rnstatus.rs ++++ b/crates/rns-tools/src/commands/rnstatus.rs +@@ -1006,7 +1006,7 @@ impl RemoteSession { + ) + .await + { +- Ok(b) => b, ++ Ok(b) => b.data, + Err(e) => { + eprintln!("rnstatus-rs: remote query failed: {}", remote_err(&e)); + return match e { diff --git a/scripts/apply-rsLXMF-file-attachments-list.sh b/scripts/apply-rsLXMF-file-attachments-list.sh new file mode 100755 index 000000000..04807c1ed --- /dev/null +++ b/scripts/apply-rsLXMF-file-attachments-list.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Apply mesh-client rsLXMF multi-file attachment pack/list overlay. +# Carries ratspeak/rsLXMF#7 on floated origin/main until upstream merges. +# Upstream: https://github.com/ratspeak/rsLXMF/pull/7 +set -euo pipefail + +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-file-attachments-list.patch" +LXMF_DIR="${RS_LXMF_DIR:-${REPO_ROOT}/.rsstack/rsLXMF}" +MESSAGE_RS="${LXMF_DIR}/crates/lxmf-core/src/message.rs" + +if [[ ! -d "${LXMF_DIR}/.git" ]]; then + echo "error: rsLXMF not found at ${LXMF_DIR}" >&2 + echo "Clone: git clone https://github.com/ratspeak/rsLXMF.git ${LXMF_DIR}" >&2 + exit 1 +fi + +if [[ ! -f "${PATCH_FILE}" ]]; then + echo "error: patch not found at ${PATCH_FILE}" >&2 + exit 1 +fi + +overlay_already_present() { + [[ -f "${MESSAGE_RS}" ]] || return 1 + grep -qE 'fn set_file_attachments_field\(' "${MESSAGE_RS}" \ + && grep -qE 'fn file_attachments\(' "${MESSAGE_RS}" +} + +if overlay_already_present; then + echo "file-attachments-list overlay already present on rsLXMF @ $(git -C "${LXMF_DIR}" rev-parse --short HEAD)" + exit 0 +fi + +if apply_ratspeak_overlay_or_die "${LXMF_DIR}" "${PATCH_FILE}" "file-attachments-list"; then + exit 0 +fi +exit 1 diff --git a/scripts/apply-rsReticulum-reply-file-query-metadata.sh b/scripts/apply-rsReticulum-reply-file-query-metadata.sh new file mode 100755 index 000000000..640382445 --- /dev/null +++ b/scripts/apply-rsReticulum-reply-file-query-metadata.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Apply mesh-client rsReticulum ReplyFile + LinkClient query metadata overlay. +# Carries ratspeak/rsReticulum#26 on floated origin/main until upstream merges +# (NomadNet /file + /media response Resources). +# Upstream: https://github.com/ratspeak/rsReticulum/pull/26 +set -euo pipefail + +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-reply-file-query-metadata.patch" +RNS_DIR="${RS_RETICULUM_DIR:-${REPO_ROOT}/.rsstack/rsReticulum}" +LINK_MANAGER_RS="${RNS_DIR}/crates/rns-runtime/src/link_manager.rs" + +if [[ ! -d "${RNS_DIR}/.git" ]]; then + echo "error: rsReticulum not found at ${RNS_DIR}" >&2 + echo "Clone: git clone https://github.com/ratspeak/rsReticulum.git ${RNS_DIR}" >&2 + exit 1 +fi + +if [[ ! -f "${PATCH_FILE}" ]]; then + echo "error: patch not found at ${PATCH_FILE}" >&2 + exit 1 +fi + +overlay_already_present() { + [[ -f "${LINK_MANAGER_RS}" ]] || return 1 + grep -qE 'enum RequestOutcome' "${LINK_MANAGER_RS}" \ + && grep -qE 'ReplyFile\s*\{' "${LINK_MANAGER_RS}" \ + && grep -qE 'fn pack_file_name_metadata\(' "${LINK_MANAGER_RS}" +} + +if overlay_already_present; then + echo "reply-file query-metadata overlay already present on rsReticulum @ $(git -C "${RNS_DIR}" rev-parse --short HEAD)" + exit 0 +fi + +if apply_ratspeak_overlay_or_die "${RNS_DIR}" "${PATCH_FILE}" "reply-file-query-metadata"; then + exit 0 +fi +exit 1 diff --git a/scripts/clone-ratspeak-stack.sh b/scripts/clone-ratspeak-stack.sh index 409f87eba..65efacff3 100755 --- a/scripts/clone-ratspeak-stack.sh +++ b/scripts/clone-ratspeak-stack.sh @@ -20,14 +20,8 @@ LRGP_DIR="${WORKSPACE_ROOT}/lrgp-rs" export RS_RETICULUM_DIR="${RNS_DIR}" export RS_LXMF_DIR="${LXMF_DIR}" -# Optional bisect / known-good overrides. Unset or empty → float to origin/main. -# In CI, load temporary stacked pins when present (local clones stay floating). -if [[ "${CI:-}" == 'true' && -f "${SCRIPT_DIR}/ratspeak-stack-ci-pins.env" ]]; then - set -a - # shellcheck disable=SC1091 - source "${SCRIPT_DIR}/ratspeak-stack-ci-pins.env" - set +a -fi +# Optional bisect overrides only. Unset or empty → float to origin/main. +# CI and pnpm run update never set these — open upstream feature PRs are overlays. RS_RETICULUM_REF="${RS_RETICULUM_REF:-}" RS_LXMF_REF="${RS_LXMF_REF:-}" RS_NOMAD_REF="${RS_NOMAD_REF:-}" diff --git a/scripts/clone-ratspeak-stack.test.mjs b/scripts/clone-ratspeak-stack.test.mjs index e8f8a2560..07d2013d2 100644 --- a/scripts/clone-ratspeak-stack.test.mjs +++ b/scripts/clone-ratspeak-stack.test.mjs @@ -88,8 +88,6 @@ function runEnsureRepo({ remoteUrl, destDir, pinRef = '', env = {}, mergeStderr GIT_CONFIG_GLOBAL: '/dev/null', // Host shells often export RS_STACK_DISCARD_DIRTY=1 while debugging clones. RS_STACK_DISCARD_DIRTY: '', - // Avoid CI pin file when sourcing the script under unit tests. - CI: '', ...env, }, }); @@ -102,8 +100,8 @@ describe('clone-ratspeak-stack.sh float policy', () => { 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('ratspeak-stack-ci-pins.env'); - expect(cloneScript).toContain('CI:-'); + expect(cloneScript).not.toContain('ratspeak-stack-ci-pins.env'); + expect(cloneScript).toContain('open upstream feature PRs are overlays'); expect(cloneScript).toContain('export RS_RETICULUM_DIR='); expect(cloneScript).toContain('export RS_LXMF_DIR='); expect(cloneScript).toContain('refuse to float/pin'); diff --git a/scripts/lib/ratspeak-overlay-apply-list.sh b/scripts/lib/ratspeak-overlay-apply-list.sh index 8c921f9b0..c5dc50cd3 100644 --- a/scripts/lib/ratspeak-overlay-apply-list.sh +++ b/scripts/lib/ratspeak-overlay-apply-list.sh @@ -4,6 +4,7 @@ # shellcheck shell=bash RS_RETICULUM_APPLY_SCRIPTS=( + apply-rsReticulum-reply-file-query-metadata.sh apply-rsReticulum-packet-tap.sh apply-rsReticulum-auto-beacon-utun.sh apply-rsReticulum-link-client-proof-budget.sh @@ -18,6 +19,7 @@ RS_RETICULUM_APPLY_SCRIPTS=( ) RS_LXMF_APPLY_SCRIPTS=( + apply-rsLXMF-file-attachments-list.sh apply-rsLXMF-propagation-sync-peering.sh apply-rsLXMF-propagation-node-policy-setters.sh apply-rsLXMF-propagation-node-deferred-messagestore-load.sh diff --git a/scripts/ratspeak-stack-ci-pins.env b/scripts/ratspeak-stack-ci-pins.env deleted file mode 100644 index 43c1b659a..000000000 --- a/scripts/ratspeak-stack-ci-pins.env +++ /dev/null @@ -1,12 +0,0 @@ -# Temporary CI pins for stacked Nomad file/metadata + media work. -# Applied only when CI=true (see clone-ratspeak-stack.sh). Clear assignments -# (or delete this file) when the matching RATSPEAK_STACK_PR_ENTRIES in -# scripts/update.sh report upstream MERGED: -# ratspeak/rsReticulum#26 ReplyFile + LinkClient::query metadata -# ratspeak/rsLXMF#7 multi-file attachment APIs -# Colorado-Mesh/rsNomad NomadNet 1.4.1 /media (feat branch tip until main) -# -# Use := so workflow env overrides still win when explicitly set. -: "${RS_RETICULUM_REF:=e16bd152256a5caffb704446bbe15530c1b20f48}" -: "${RS_LXMF_REF:=c3d8b44942e7726dbbe6bb53e0976d4c72134119}" -: "${RS_NOMAD_REF:=ec6b6dde23addf9d8248b794238baaa6498fe171}" diff --git a/scripts/update.sh b/scripts/update.sh index 4bbadf823..651e4ad0d 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -253,6 +253,7 @@ fi check_ratspeak_patches() { # Format: "patch-basename|github-owner/repo|pr-number-or-empty|display-label|review-url" local RATSPEAK_PATCH_ENTRIES=( + 'rsReticulum-reply-file-query-metadata.patch|ratspeak/rsReticulum|26|rsReticulum ReplyFile / LinkClient query metadata|https://github.com/ratspeak/rsReticulum/pull/26' 'rsReticulum-packet-tap.patch|ratspeak/rsReticulum|10|rsReticulum packet-tap|https://github.com/ratspeak/rsReticulum/pull/10' 'rsReticulum-path-medium-slots.patch|ratspeak/rsReticulum||rsReticulum path-medium slots|' 'rsReticulum-auto-beacon-utun.patch|ratspeak/rsReticulum|11|rsReticulum auto-beacon utun|https://github.com/ratspeak/rsReticulum/pull/11' @@ -264,6 +265,7 @@ check_ratspeak_patches() { 'rsReticulum-interface-tx-queue-stats.patch|ratspeak/rsReticulum||rsReticulum interface TX queue stats|' 'rsReticulum-announce-rebroadcast-exclude-rf.patch|ratspeak/rsReticulum||rsReticulum announce rebroadcast exclude RF sinks (ratspeak/rsReticulum#24)|https://github.com/ratspeak/rsReticulum/issues/24' 'rsReticulum-ble-rnode-flow-control-ready-timeout.patch|ratspeak/rsReticulum||rsReticulum BLE RNode flow-control READY timeout|' + 'rsLXMF-file-attachments-list.patch|ratspeak/rsLXMF|7|rsLXMF multi-file attachment APIs|https://github.com/ratspeak/rsLXMF/pull/7' 'rsLXMF-propagation-sync-peering.patch|ratspeak/rsLXMF|4|rsLXMF propagation sync peering|https://github.com/ratspeak/rsLXMF/pull/4' 'rsLXMF-propagation-node-policy-setters.patch|ratspeak/rsLXMF|6|rsLXMF PropagationNode policy setters|https://github.com/ratspeak/rsLXMF/pull/6' 'rsLXMF-propagation-node-deferred-messagestore-load.patch|ratspeak/rsLXMF||rsLXMF PropagationNode deferred messagestore load|' @@ -373,71 +375,6 @@ check_ratspeak_patches() { fi } -# Track stacked upstream feature PRs that mesh-client pins until they land on main -# (scripts/ratspeak-stack-ci-pins.env + optional workflow env). Not overlay patches — -# these are dependency APIs (ReplyFile, multi-file LXMF attachments, …). -# Format: "owner/repo|pr-number|display-label|cleanup-hint" -# Keep in sync with scripts/ratspeak-stack-ci-pins.env and reticulum-sidecar/patches/README.md. -RATSPEAK_STACK_PR_ENTRIES=( - 'ratspeak/rsReticulum|26|rsReticulum ReplyFile / LinkClient query metadata|clear RS_RETICULUM_REF from scripts/ratspeak-stack-ci-pins.env (+ reticulum-sidecar.yaml / flatpak.yaml env pins)' - 'ratspeak/rsLXMF|7|rsLXMF multi-file attachment APIs|clear RS_LXMF_REF from scripts/ratspeak-stack-ci-pins.env (+ reticulum-sidecar.yaml / flatpak.yaml env pins)' - 'Colorado-Mesh/rsNomad|8|rsNomad NomadNet 1.4.1 /media ACL CGI|clear RS_NOMAD_REF from scripts/ratspeak-stack-ci-pins.env (+ reticulum-sidecar.yaml / flatpak.yaml env pins)' -) - -check_ratspeak_stack_prs() { - local has_stack_pr_warning=0 - local entry repo pr label cleanup url state - - echo '' - echo 'Checking stacked Ratspeak feature PRs (CI pins until merge)...' - - if [ "${#RATSPEAK_STACK_PR_ENTRIES[@]}" -eq 0 ]; then - echo ' No stacked feature PRs tracked.' - return 0 - fi - - for entry in "${RATSPEAK_STACK_PR_ENTRIES[@]}"; do - IFS='|' read -r repo pr label cleanup <<< "${entry}" - url="https://github.com/${repo}/pull/${pr}" - state="$(github_pr_state "${repo}" "${pr}")" - case "${state}" in - open) - echo " ${label}: still open — ${url}" - echo " CI pins via scripts/ratspeak-stack-ci-pins.env (and matching workflow env)." - ;; - merged) - warn_box "${label} (stacked feature PR)" "CI pin" "upstream MERGED" "${url}" - echo " Reason tracked: ${repo}#${pr} merged — ${cleanup}" - echo " then drop this entry from RATSPEAK_STACK_PR_ENTRIES in scripts/update.sh." - has_stack_pr_warning=1 - HAS_WARNING=1 - ;; - closed) - warn_box "${label} (stacked feature PR)" "CI pin" "PR closed (not merged?)" "${url}" - echo " Reason tracked: ${repo}#${pr} closed without merge — verify pin still needed," - echo " then ${cleanup} and drop entry from RATSPEAK_STACK_PR_ENTRIES." - has_stack_pr_warning=1 - HAS_WARNING=1 - ;; - *) - echo " ${label}: could not query ${repo}#${pr} (install gh or check network) — ${url}" - ;; - esac - done - - if [ "${has_stack_pr_warning}" -eq 0 ]; then - echo ' Stacked feature PR check complete (pins still match open upstream).' - fi -} - -# Test hook: exercise check_ratspeak_stack_prs (fake gh/curl via PATH). -if [ "${UPDATE_SH_TEST_HOOK:-}" = 'stack-prs-only' ]; then - HAS_WARNING=0 - check_ratspeak_stack_prs - printf 'HAS_WARNING=%s\n' "${HAS_WARNING}" - exit 0 -fi - # GET GitHub API path (gh preferred, curl fallback). Body on stdout. # Exit 0 = body (may be empty), exit 2 = rate-limit payload detected (empty body). # Callers must handle exit 2 in the parent shell (command substitution drops side effects). @@ -654,10 +591,6 @@ RATSPEAK_KNOWN_ORG_REPOS=( print_ratspeak_upstream_catalog() { local entry - echo 'RATSPEAK_STACK_PR_ENTRIES:' - for entry in "${RATSPEAK_STACK_PR_ENTRIES[@]}"; do - echo " ${entry}" - done echo 'RATSPEAK_RELEASE_WATCH_ENTRIES:' for entry in "${RATSPEAK_RELEASE_WATCH_ENTRIES[@]}"; do echo " ${entry}" @@ -907,7 +840,6 @@ done check_pinned_majors check_ratspeak_patches -check_ratspeak_stack_prs check_ratspeak_upstream if [ "${HAS_WARNING}" -eq 0 ]; then diff --git a/scripts/update.test.mjs b/scripts/update.test.mjs index 71909e095..85972934a 100644 --- a/scripts/update.test.mjs +++ b/scripts/update.test.mjs @@ -104,10 +104,7 @@ describe('update.sh Reticulum stack functionality check', () => { 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_STACK_PR_ENTRIES:'); - expect(result.stdout).toContain('ratspeak/rsReticulum|26|'); - expect(result.stdout).toContain('Colorado-Mesh/rsNomad|8|'); - expect(result.stdout).toContain('ratspeak/rsLXMF|7|'); + expect(result.stdout).not.toContain('RATSPEAK_STACK_PR_ENTRIES:'); expect(result.stdout).toContain('RATSPEAK_RELEASE_WATCH_ENTRIES:'); expect(result.stdout).toContain('ratspeak/rsLXST||rsLXST voice (lxst-telephony)|v0.2.0'); expect(result.stdout).toContain('ratspeak/lrgp-rs||lrgp-rs games (LRGP)|v0.4.1'); @@ -126,80 +123,22 @@ describe('update.sh Reticulum stack functionality check', () => { expect(result.stdout).toContain(' lrgp-rs'); }); - it('wires check_ratspeak_stack_prs between overlay and upstream checks', () => { - expect(updateScript).toContain('check_ratspeak_stack_prs()'); - expect(updateScript).toContain('RATSPEAK_STACK_PR_ENTRIES'); - expect(updateScript).toContain('ratspeak/rsReticulum|26|'); - expect(updateScript).toContain('ratspeak/rsLXMF|7|'); - expect(updateScript).toContain('Colorado-Mesh/rsNomad|8|'); - expect(updateScript).toContain('ratspeak-stack-ci-pins.env'); - const patchesCall = updateScript.lastIndexOf('\ncheck_ratspeak_patches\n'); - const stackPrsCall = updateScript.lastIndexOf('\ncheck_ratspeak_stack_prs\n'); - const upstreamCall = updateScript.lastIndexOf('\ncheck_ratspeak_upstream\n'); - expect(patchesCall).toBeGreaterThanOrEqual(0); - expect(stackPrsCall).toBeGreaterThan(patchesCall); - expect(upstreamCall).toBeGreaterThan(stackPrsCall); - }); - - it('stack-prs-only reports open pins without warning', () => { - const binDir = mkdtempSync(path.join(os.tmpdir(), 'mesh-update-stack-prs-')); - tempDirs.push(binDir); - const ghPath = path.join(binDir, 'gh'); - writeFileSync( - ghPath, - `#!/bin/bash -# Fake gh api for stack PR state -if [[ "$*" == *repos/ratspeak/rsReticulum/pulls/26* ]] || [[ "$*" == *repos/ratspeak/rsLXMF/pulls/7* ]]; then - printf '%s' '{"state":"open","merged":false}' - exit 0 -fi -printf '%s' '{}' -exit 0 -`, - 'utf8', + it('tracks ReplyFile and multi-file attachment overlays in RATSPEAK_PATCH_ENTRIES', () => { + expect(updateScript).toContain( + 'rsReticulum-reply-file-query-metadata.patch|ratspeak/rsReticulum|26|', ); - chmodSync(ghPath, 0o755); - const result = runUpdate([], { - UPDATE_SH_TEST_HOOK: 'stack-prs-only', - PATH: `${binDir}:${process.env.PATH ?? ''}`, - }); - expect(result.status, result.stderr || result.stdout).toBe(0); - expect(result.stdout).toContain('still open'); - expect(result.stdout).toContain('rsReticulum ReplyFile'); - expect(result.stdout).toContain('rsLXMF multi-file'); - expect(result.stdout).toContain('HAS_WARNING=0'); - expect(result.stdout).not.toContain('WARNING:'); + expect(updateScript).toContain('rsLXMF-file-attachments-list.patch|ratspeak/rsLXMF|7|'); + expect(updateScript).not.toContain('RATSPEAK_STACK_PR_ENTRIES'); + expect(updateScript).not.toContain('check_ratspeak_stack_prs'); + expect(updateScript).not.toContain('ratspeak-stack-ci-pins.env'); }); - it('stack-prs-only warns when a stacked PR is merged', () => { - const binDir = mkdtempSync(path.join(os.tmpdir(), 'mesh-update-stack-merged-')); - tempDirs.push(binDir); - const ghPath = path.join(binDir, 'gh'); - writeFileSync( - ghPath, - `#!/bin/bash -if [[ "$*" == *repos/ratspeak/rsReticulum/pulls/26* ]]; then - printf '%s' '{"state":"closed","merged":true,"merged_at":"2026-09-09T00:00:00Z"}' - exit 0 -fi -if [[ "$*" == *repos/ratspeak/rsLXMF/pulls/7* ]]; then - printf '%s' '{"state":"open","merged":false}' - exit 0 -fi -printf '%s' '{}' -exit 0 -`, - 'utf8', - ); - chmodSync(ghPath, 0o755); - const result = runUpdate([], { - UPDATE_SH_TEST_HOOK: 'stack-prs-only', - PATH: `${binDir}:${process.env.PATH ?? ''}`, - }); - expect(result.status, result.stderr || result.stdout).toBe(0); - expect(result.stdout).toContain('upstream MERGED'); - expect(result.stdout).toContain('ratspeak-stack-ci-pins.env'); - expect(result.stdout).toContain('HAS_WARNING=1'); + it('wires check_ratspeak_patches before upstream checks', () => { + expect(updateScript).toContain('check_ratspeak_patches()'); + 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('wires check_ratspeak_upstream after overlay PR checks', () => { From 183dc7ebc99e3f9a1e72836a6f2182f0a6400bc4 Mon Sep 17 00:00:00 2001 From: Joey Stanford Date: Fri, 11 Sep 2026 05:49:31 -0600 Subject: [PATCH 4/6] chore(deps): refresh lockfiles after pnpm update Bump matured npm ranges from pnpm update and sync sidecar Cargo.lock with floated Ratspeak/Nomad (nomad-core libc). --- package.json | 26 +- pnpm-lock.yaml | 708 +++++++++++++++++------------------ reticulum-sidecar/Cargo.lock | 1 + 3 files changed, 368 insertions(+), 367 deletions(-) diff --git a/package.json b/package.json index caebea46a..d40d1fb97 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "@stoprocent/noble": "^2.8.0", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", - "@zip.js/zip.js": "^2.11.0", + "@zip.js/zip.js": "^2.14.0", "builder-util-runtime": "^9.7.0", "dompurify": "^3.4.15", "electron-updater": "^6.8.9", @@ -158,7 +158,7 @@ "i18next": "^26.4.2", "js-md5": "^0.8.3", "jsqr": "^1.4.0", - "jszip": "^3.10.1", + "jszip": "^3.10.2", "leaflet.markercluster": "^1.5.3", "lucide-react-motion": "^0.4.0", "mgrs": "^2.2.0", @@ -170,7 +170,7 @@ "react-leaflet-cluster": "^4.1.3", "readable-stream": "^4.7.0", "semver": "^7.8.5", - "systeminformation": "^5.33.8", + "systeminformation": "^5.33.10", "undici": "^8.10.2" }, "devDependencies": { @@ -183,24 +183,24 @@ "@michaelhart/meshcore-decoder": "^0.3.0", "@playwright/test": "^1.63.0", "@tailwindcss/vite": "^4.3.3", - "@tanstack/react-virtual": "^3.14.10", + "@tanstack/react-virtual": "^3.14.11", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@testing-library/user-event": "^14.6.7", "@types/js-md5": "^0.8.0", "@types/leaflet": "^1.9.22", - "@types/node": "^25.9.5", + "@types/node": "^25.9.6", "@types/node-forge": "^1.3.14", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.7", - "@typescript-eslint/eslint-plugin": "^8.69.0", - "@typescript-eslint/parser": "^8.69.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@typescript-eslint/eslint-plugin": "^8.70.0", + "@typescript-eslint/parser": "^8.70.0", "@vitejs/plugin-react": "^6.1.1", "@vitest/coverage-v8": "^4.1.11", "concurrently": "^9.2.4", "electron": "^44.1.1", - "electron-builder": "^26.16.0", + "electron-builder": "^26.16.1", "esbuild": "^0.28.2", "eslint": "^10.10.0", "eslint-config-prettier": "^10.1.8", @@ -220,14 +220,14 @@ "prettier": "^3.9.6", "prettier-plugin-sh": "^0.18.1", "prettier-plugin-tailwindcss": "^0.7.4", - "react": "^19.2.8", - "react-dom": "^19.2.8", + "react": "^19.3.0", + "react-dom": "^19.3.0", "react-leaflet": "^5.0.0", "recharts": "^3.10.1", "sort-package-json": "^3.7.1", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", - "typescript-eslint": "^8.69.0", + "typescript-eslint": "^8.70.0", "vite": "^8.2.2", "vitest": "^4.1.11", "vitest-axe": "^1.0.0-pre.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03d594170..e29ad6d0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,8 +183,8 @@ importers: specifier: ^6.0.0 version: 6.0.0 '@zip.js/zip.js': - specifier: ^2.11.0 - version: 2.11.2 + specifier: ^2.14.0 + version: 2.14.0 builder-util-runtime: specifier: ^9.7.0 version: 9.7.0(supports-color@8.1.1) @@ -210,20 +210,20 @@ importers: specifier: ^1.4.0 version: 1.4.0 jszip: - specifier: ^3.10.1 - version: 3.10.1 + specifier: ^3.10.2 + version: 3.10.2 leaflet.markercluster: specifier: ^1.5.3 version: 1.5.3(leaflet@1.9.4) lucide-react-motion: specifier: ^0.4.0 - version: 0.4.0(motion@12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 0.4.0(motion@12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) mgrs: specifier: ^2.2.0 version: 2.2.0 motion: specifier: ^12.43.0 - version: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) mqtt: specifier: ^5.15.2 version: 5.15.2(supports-color@8.1.1) @@ -235,10 +235,10 @@ importers: version: 1.5.4 react-i18next: specifier: ^17.0.13 - version: 17.0.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) + version: 17.0.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3) react-leaflet-cluster: specifier: ^4.1.3 - version: 4.1.3(@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + version: 4.1.3(@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0) readable-stream: specifier: ^4.7.0 version: 4.7.0(patch_hash=96023d9278085d7490d08bce1a5079dd202f7782a0daa66fa81c6b1424ef8ab1) @@ -246,8 +246,8 @@ importers: specifier: ^7.8.5 version: 7.8.5 systeminformation: - specifier: ^5.33.8 - version: 5.33.8 + specifier: ^5.33.10 + version: 5.33.10 undici: specifier: ^8.10.2 version: 8.10.2 @@ -278,16 +278,16 @@ importers: version: 1.63.0 '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.3.3(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) '@tanstack/react-virtual': - specifier: ^3.14.10 - version: 3.14.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^3.14.11 + version: 3.14.11(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@testing-library/jest-dom': specifier: ^7.0.1 version: 7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.11) '@testing-library/react': specifier: ^16.3.3 - version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@testing-library/user-event': specifier: ^14.6.7 version: 14.6.7(@testing-library/dom@10.4.1) @@ -298,8 +298,8 @@ importers: specifier: ^1.9.22 version: 1.9.22 '@types/node': - specifier: ^25.9.5 - version: 25.9.5 + specifier: ^25.9.6 + version: 25.9.6 '@types/node-forge': specifier: ^1.3.14 version: 1.3.14 @@ -307,20 +307,20 @@ importers: specifier: ^1.5.6 version: 1.5.6 '@types/react': - specifier: ^19.2.18 - version: 19.2.18 + specifier: ^19.3.0 + version: 19.3.0 '@types/react-dom': - specifier: ^19.2.7 - version: 19.2.7(@types/react@19.2.18) + specifier: ^19.3.0 + version: 19.3.0(@types/react@19.3.0) '@typescript-eslint/eslint-plugin': - specifier: ^8.69.0 - version: 8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + specifier: ^8.70.0 + version: 8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) '@typescript-eslint/parser': - specifier: ^8.69.0 - version: 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + specifier: ^8.70.0 + version: 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) '@vitejs/plugin-react': specifier: ^6.1.1 - version: 6.1.1(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 6.1.1(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: ^4.1.11 version: 4.1.11(vitest@4.1.11) @@ -329,10 +329,10 @@ importers: version: 9.2.4 electron: specifier: ^44.1.0 - version: 44.2.0(supports-color@8.1.1) + version: 44.3.0(supports-color@8.1.1) electron-builder: - specifier: ^26.16.0 - version: 26.16.0(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) + specifier: ^26.16.1 + version: 26.16.1(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) esbuild: specifier: ^0.28.2 version: 0.28.2 @@ -347,7 +347,7 @@ importers: version: 7.0.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + version: 2.32.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) eslint-plugin-jsx-a11y: specifier: ^6.10.2 version: 6.10.2(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1)) @@ -391,17 +391,17 @@ importers: specifier: ^0.7.4 version: 0.7.4(prettier@3.9.6) react: - specifier: ^19.2.8 - version: 19.2.8 + specifier: ^19.3.0 + version: 19.3.0 react-dom: - specifier: ^19.2.8 - version: 19.2.8(react@19.2.8) + specifier: ^19.3.0 + version: 19.3.0(react@19.3.0) react-leaflet: specifier: ^5.0.0 - version: 5.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 5.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) recharts: specifier: ^3.10.1 - version: 3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@17.0.2)(react@19.2.8)(redux@5.0.1) + version: 3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@17.0.2)(react@19.3.0)(redux@5.0.1) sort-package-json: specifier: ^3.7.1 version: 3.7.1 @@ -412,20 +412,20 @@ importers: specifier: ^6.0.3 version: 6.0.3 typescript-eslint: - specifier: ^8.69.0 - version: 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + specifier: ^8.70.0 + version: 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) vite: specifier: ^8.2.2 - version: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + version: 8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) vitest: specifier: ^4.1.11 - version: 4.1.11(@types/node@25.9.5)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.11(@types/node@25.9.6)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) vitest-axe: specifier: ^1.0.0-pre.5 version: 1.0.0-pre.5(vitest@4.1.11) zustand: specifier: ^5.0.15 - version: 5.0.15(@types/react@19.2.18)(immer@11.1.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + version: 5.0.15(@types/react@19.3.0)(immer@11.1.18)(react@19.3.0)(use-sync-external-store@1.7.0(react@19.3.0)) packages: @@ -929,8 +929,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.148.0': - resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + '@oxc-project/types@0.149.0': + resolution: {integrity: sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==} '@peculiar/asn1-schema@2.9.4': resolution: {integrity: sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==} @@ -999,98 +999,98 @@ packages: engines: {node: ^v12.20.0 || ^14.13.0 || >=16.0.0} hasBin: true - '@rolldown/binding-android-arm-eabi@1.2.7': - resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} + '@rolldown/binding-android-arm-eabi@1.2.8': + resolution: {integrity: sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rolldown/binding-android-arm64@1.2.7': - resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==} + '@rolldown/binding-android-arm64@1.2.8': + resolution: {integrity: sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.7': - resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==} + '@rolldown/binding-darwin-arm64@1.2.8': + resolution: {integrity: sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.7': - resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==} + '@rolldown/binding-darwin-x64@1.2.8': + resolution: {integrity: sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.7': - resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==} + '@rolldown/binding-freebsd-x64@1.2.8': + resolution: {integrity: sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.7': - resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.8': + resolution: {integrity: sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.7': - resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==} + '@rolldown/binding-linux-arm64-gnu@1.2.8': + resolution: {integrity: sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.7': - resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} + '@rolldown/binding-linux-arm64-musl@1.2.8': + resolution: {integrity: sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.7': - resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.8': + resolution: {integrity: sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.7': - resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} + '@rolldown/binding-linux-s390x-gnu@1.2.8': + resolution: {integrity: sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.7': - resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} + '@rolldown/binding-linux-x64-gnu@1.2.8': + resolution: {integrity: sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.7': - resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} + '@rolldown/binding-linux-x64-musl@1.2.8': + resolution: {integrity: sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.7': - resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} + '@rolldown/binding-openharmony-arm64@1.2.8': + resolution: {integrity: sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.7': - resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==} + '@rolldown/binding-win32-arm64-msvc@1.2.8': + resolution: {integrity: sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.7': - resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==} + '@rolldown/binding-win32-x64-msvc@1.2.8': + resolution: {integrity: sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1338,14 +1338,14 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@tanstack/react-virtual@3.14.10': - resolution: {integrity: sha512-SRyoUbdFMRHuYXMijV5H4ZarQWpXkj3iANq8OFre+pybeVap8ZJjZ3Nz9bVjx4d8PfobVUQUdKyyyHYk3E+djw==} + '@tanstack/react-virtual@3.14.11': + resolution: {integrity: sha512-SStWf8bYdTgAquYqG4Pi2+d1XimQKoCIJ94H3jsm0D6UA0yqffvWuvUzrrQ4idewFWq8BSPpahnmLVosJIAs6g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/virtual-core@3.17.8': - resolution: {integrity: sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA==} + '@tanstack/virtual-core@3.17.9': + resolution: {integrity: sha512-M8Bzy7CCMvUjRiuoHVH9mRjyUVczRs8v8RcUEphLFGc445ZP4KQ15Y1sLqhQqJi/HgGJrxfCHnEnIX0x9mVrBg==} '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} @@ -1463,22 +1463,22 @@ packages: '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} - '@types/node@24.13.3': - resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + '@types/node@24.13.4': + resolution: {integrity: sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==} - '@types/node@25.9.5': - resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + '@types/node@25.9.6': + resolution: {integrity: sha512-JR6Q/PV5DKFvjrGFVqQJdeG0qvsqQQLDa3TzFrqVwhqRXqwNaxPo2KYCtCQXpOdIulCouKwT7a6in9nFthBAzw==} '@types/qrcode@1.5.6': resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} - '@types/react-dom@19.2.7': - resolution: {integrity: sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==} + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} peerDependencies: - '@types/react': ^19.2.0 + '@types/react': ^19.3.0 - '@types/react@19.2.18': - resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} '@types/readable-stream@4.0.24': resolution: {integrity: sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==} @@ -1501,63 +1501,63 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.69.0': - resolution: {integrity: sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==} + '@typescript-eslint/eslint-plugin@8.70.0': + resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.69.0 + '@typescript-eslint/parser': ^8.70.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.69.0': - resolution: {integrity: sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==} + '@typescript-eslint/parser@8.70.0': + resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.69.0': - resolution: {integrity: sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==} + '@typescript-eslint/project-service@8.70.0': + resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.69.0': - resolution: {integrity: sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==} + '@typescript-eslint/scope-manager@8.70.0': + resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.69.0': - resolution: {integrity: sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==} + '@typescript-eslint/tsconfig-utils@8.70.0': + resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.69.0': - resolution: {integrity: sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==} + '@typescript-eslint/type-utils@8.70.0': + resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.69.0': - resolution: {integrity: sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==} + '@typescript-eslint/types@8.70.0': + resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.69.0': - resolution: {integrity: sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==} + '@typescript-eslint/typescript-estree@8.70.0': + resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.69.0': - resolution: {integrity: sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==} + '@typescript-eslint/utils@8.70.0': + resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.69.0': - resolution: {integrity: sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==} + '@typescript-eslint/visitor-keys@8.70.0': + resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-react@6.1.1': @@ -1630,8 +1630,8 @@ packages: '@yarnpkg/lockfile@1.1.0': resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} - '@zip.js/zip.js@2.11.2': - resolution: {integrity: sha512-gvlhhtTzqoGcl3bpFpMieJgFiVGnEgD777JgERXhbU89cCnYc2Srl5gs+kcCoy31hfx+xD4UCDO1MB2+UtqcZA==} + '@zip.js/zip.js@2.14.0': + resolution: {integrity: sha512-7HIW+xGAl6LeTlgfLEKeRBPBskNtLSXRNXqb0QuUv6kYO7hhJvSda1UfYvyfyKJxod3q5GRvYZjjdtkxO1FMiQ==} engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} abbrev@4.0.0: @@ -1699,8 +1699,8 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + array-includes@3.2.0: + resolution: {integrity: sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==} engines: {node: '>= 0.4'} array.prototype.findlast@1.2.5: @@ -1738,8 +1738,8 @@ packages: ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - ast-v8-to-istanbul@1.0.5: - resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + ast-v8-to-istanbul@1.0.6: + resolution: {integrity: sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==} async-exit-hook@2.0.1: resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} @@ -2158,8 +2158,8 @@ packages: dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} - dmg-builder@26.16.0: - resolution: {integrity: sha512-1s7GZgxPNLLxStA0wu2a4gpIhgmOai+UL72iACha+uyQDbdRcGTjOZcN3PCJ8c/P+Z2cESSZTmUM1B0xop9cqQ==} + dmg-builder@26.16.1: + resolution: {integrity: sha512-pnI/3Qb24Uk+rMTgIUrsVUKosVgwmBUdF8Zeb8TexOSbpq8MWc7v6l+n+FrEqVkjNZwzBN+XpDS9ENgZ/rkWAw==} doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} @@ -2197,16 +2197,16 @@ packages: electron-builder-squirrel-windows@26.15.3: resolution: {integrity: sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==} - electron-builder@26.16.0: - resolution: {integrity: sha512-FQAUWjdcLMXtCKYkvVqhV5N+EH1uiw50RxJnrLDZtDiOykW8ywhkrbysSi7J3mZjp+Jb2B7Y1pQD3urOEYXvIg==} + electron-builder@26.16.1: + resolution: {integrity: sha512-LrLK65QX5PUYYODXqp23FKrV7CILTtVY7mrJckNknO9jLNSMiqFkKbSMiDRw4CjOADMPVDdWLxY4mezOZWswxg==} engines: {node: '>=14.0.0'} hasBin: true electron-publish@26.15.3: resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} - electron-to-chromium@1.5.422: - resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==} + electron-to-chromium@1.5.426: + resolution: {integrity: sha512-2Gcq6inCQs/AqfHP3f5ftCzk+pqeW2VlA1LgmPqEj2hfBO8HiZRspNYkD4HzFBe4u6lGqca4BspFr6Ix4Q400g==} electron-updater@6.8.9: resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==} @@ -2215,8 +2215,8 @@ packages: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} - electron@44.2.0: - resolution: {integrity: sha512-oK1icjhapp3xsUZycO9WZaLmd3hJnA7ieobC4mpdtO1pEN+PPGWpA3m1Mgzzuc1wzSD2Wai4zcH4yfpGJqxFMA==} + electron@44.3.0: + resolution: {integrity: sha512-St9EV7F2VtYaYWD2qaAjBwUgKxx39eJOUsUJ5+/1113sqbVfNqv4Dbm/W1rN7qmYSPa+mWwR6yr+b7MfgjgVfQ==} engines: {node: '>= 22.12.0'} hasBin: true @@ -2237,8 +2237,8 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@8.0.0: - resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} engines: {node: '>=20.19.0'} env-paths@2.2.1: @@ -2763,8 +2763,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.8: - resolution: {integrity: sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==} + ignore@7.0.9: + resolution: {integrity: sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==} engines: {node: '>= 4'} immediate@3.0.6: @@ -3071,8 +3071,8 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - jszip@3.10.1: - resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + jszip@3.10.2: + resolution: {integrity: sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==} katex@0.16.47: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} @@ -3589,8 +3589,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.54: - resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + node-releases@2.0.55: + resolution: {integrity: sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==} engines: {node: '>=18'} nopt@9.0.0: @@ -3637,8 +3637,8 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} once@1.4.0: @@ -3903,10 +3903,10 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - react-dom@19.2.8: - resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} peerDependencies: - react: ^19.2.8 + react: ^19.3.0 react-i18next@17.0.13: resolution: {integrity: sha512-Cc1PscmblIHA1kljTqDwrcVMI21ydgmUzw0UAeQBe7pAOgfuRLfzXze4EUBQoeDiICzFIXXhHFoZxuetNg5D0Q==} @@ -3958,8 +3958,8 @@ packages: redux: optional: true - react@19.2.8: - resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} engines: {node: '>=0.10.0'} read-binary-file-arch@1.0.6: @@ -4069,8 +4069,8 @@ packages: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} - rolldown@1.2.7: - resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==} + rolldown@1.2.8: + resolution: {integrity: sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -4112,8 +4112,8 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} semver-compare@1.0.0: resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} @@ -4352,8 +4352,8 @@ packages: resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} engines: {node: ^14.18.0 || >=16.0.0} - systeminformation@5.33.8: - resolution: {integrity: sha512-v4F6OGYGh7wDvV68YmjOmZwGixV9A/GQ7d2b84t0UF4CaOy9jipNWIJDkHqYDYiTPuiojqlwVQd0hfUKOUN7tQ==} + systeminformation@5.33.10: + resolution: {integrity: sha512-/NXbMVASt2UbSVgmWto4aBTIAeuYY7zOELpZybmNxqpsrbo5uYHpuEPf5nkyrPng0uG5YOb+Yv6s0FyKY4mDQg==} engines: {node: '>=10.0.0'} os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true @@ -4480,8 +4480,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.69.0: - resolution: {integrity: sha512-B3MltX0VqjUBNEe3b3sSuiRbfa6XrfHFtBiPamjT5AsW/dfq+y+bc0wyuS9DxAS1LyzCxRp2+rxzpLUvqM2BvA==} + typescript-eslint@8.70.0: + resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4538,8 +4538,8 @@ packages: resolution: {integrity: sha512-klAJPUTaSxRvTgrIh7om6UTrgRxdzioD+rJc/MaoiN8+OGdqRzai39tR00asnoCesPNikItWB9zBZoo0pJsWaA==} engines: {node: '>=12.22.0 <13.0 || >=14.17.0'} - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + use-sync-external-store@1.7.0: + resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4801,8 +4801,8 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@4.5.4: - resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==} + zod@4.6.1: + resolution: {integrity: sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q==} zustand@5.0.15: resolution: {integrity: sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==} @@ -5330,7 +5330,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.3 - '@oxc-project/types@0.148.0': {} + '@oxc-project/types@0.149.0': {} '@peculiar/asn1-schema@2.9.4': dependencies: @@ -5360,13 +5360,13 @@ snapshots: dependencies: playwright: 1.63.0 - '@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: leaflet: 1.9.4 - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) - '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1))(react@19.2.8)': + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1))(react@19.3.0)': dependencies: '@standard-schema/spec': 1.1.0 '@standard-schema/utils': 0.3.0 @@ -5375,8 +5375,8 @@ snapshots: redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.2.0 optionalDependencies: - react: 19.2.8 - react-redux: 9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1) + react: 19.3.0 + react-redux: 9.3.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1) '@reteps/dockerfmt-darwin-arm64@0.5.4': optional: true @@ -5397,49 +5397,49 @@ snapshots: '@reteps/dockerfmt-linux-arm64': 0.5.4 '@reteps/dockerfmt-linux-x64': 0.5.4 - '@rolldown/binding-android-arm-eabi@1.2.7': + '@rolldown/binding-android-arm-eabi@1.2.8': optional: true - '@rolldown/binding-android-arm64@1.2.7': + '@rolldown/binding-android-arm64@1.2.8': optional: true - '@rolldown/binding-darwin-arm64@1.2.7': + '@rolldown/binding-darwin-arm64@1.2.8': optional: true - '@rolldown/binding-darwin-x64@1.2.7': + '@rolldown/binding-darwin-x64@1.2.8': optional: true - '@rolldown/binding-freebsd-x64@1.2.7': + '@rolldown/binding-freebsd-x64@1.2.8': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + '@rolldown/binding-linux-arm-gnueabihf@1.2.8': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.7': + '@rolldown/binding-linux-arm64-gnu@1.2.8': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.7': + '@rolldown/binding-linux-arm64-musl@1.2.8': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.7': + '@rolldown/binding-linux-ppc64-gnu@1.2.8': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.7': + '@rolldown/binding-linux-s390x-gnu@1.2.8': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.7': + '@rolldown/binding-linux-x64-gnu@1.2.8': optional: true - '@rolldown/binding-linux-x64-musl@1.2.7': + '@rolldown/binding-linux-x64-musl@1.2.8': optional: true - '@rolldown/binding-openharmony-arm64@1.2.7': + '@rolldown/binding-openharmony-arm64@1.2.8': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.7': + '@rolldown/binding-win32-arm64-msvc@1.2.8': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.7': + '@rolldown/binding-win32-x64-msvc@1.2.8': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -5649,20 +5649,20 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) - '@tanstack/react-virtual@3.14.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@tanstack/react-virtual@3.14.11(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: - '@tanstack/virtual-core': 3.17.8 - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) + '@tanstack/virtual-core': 3.17.9 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) - '@tanstack/virtual-core@3.17.8': {} + '@tanstack/virtual-core@3.17.9': {} '@testing-library/dom@10.4.1': dependencies: @@ -5685,17 +5685,17 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 optionalDependencies: - vitest: 4.1.11(@types/node@25.9.5)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.11(@types/node@25.9.6)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) - '@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: '@babel/runtime': 7.29.7 '@testing-library/dom': 10.4.1 - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.7(@types/react@19.2.18) + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) '@testing-library/user-event@14.6.7(@testing-library/dom@10.4.1)': dependencies: @@ -5707,7 +5707,7 @@ snapshots: dependencies: '@types/http-cache-semantics': 4.2.0 '@types/keyv': 3.1.4 - '@types/node': 25.9.5 + '@types/node': 25.9.6 '@types/responselike': 1.0.3 '@types/chai@5.2.3': @@ -5751,7 +5751,7 @@ snapshots: '@types/fs-extra@9.0.13': dependencies: - '@types/node': 25.9.5 + '@types/node': 25.9.6 '@types/geojson@7946.0.16': {} @@ -5767,7 +5767,7 @@ snapshots: '@types/keyv@3.1.4': dependencies: - '@types/node': 25.9.5 + '@types/node': 25.9.6 '@types/leaflet@1.9.22': dependencies: @@ -5777,35 +5777,35 @@ snapshots: '@types/node-forge@1.3.14': dependencies: - '@types/node': 25.9.5 + '@types/node': 25.9.6 - '@types/node@24.13.3': + '@types/node@24.13.4': dependencies: undici-types: 7.29.1 - '@types/node@25.9.5': + '@types/node@25.9.6': dependencies: undici-types: 7.29.1 '@types/qrcode@1.5.6': dependencies: - '@types/node': 25.9.5 + '@types/node': 25.9.6 - '@types/react-dom@19.2.7(@types/react@19.2.18)': + '@types/react-dom@19.3.0(@types/react@19.3.0)': dependencies: - '@types/react': 19.2.18 + '@types/react': 19.3.0 - '@types/react@19.2.18': + '@types/react@19.3.0': dependencies: csstype: 3.2.3 '@types/readable-stream@4.0.24': dependencies: - '@types/node': 25.9.5 + '@types/node': 25.9.6 '@types/responselike@1.0.3': dependencies: - '@types/node': 25.9.5 + '@types/node': 25.9.6 '@types/trusted-types@2.0.7': optional: true @@ -5819,59 +5819,59 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.9.5 + '@types/node': 25.9.6 - '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.69.0 - '@typescript-eslint/type-utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.69.0 + '@typescript-eslint/parser': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/type-utils': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.70.0 eslint: 10.10.0(jiti@2.7.0)(supports-color@8.1.1) - ignore: 7.0.8 + ignore: 7.0.9 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.69.0 - '@typescript-eslint/types': 8.69.0 - '@typescript-eslint/typescript-estree': 8.69.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.69.0 + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.70.0 debug: 4.4.3(patch_hash=cf37fa96f5df733456b16c82c9e1c9054a92f6216692aa33d5e2e1e208888e37)(supports-color@8.1.1) eslint: 10.10.0(jiti@2.7.0)(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.69.0(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/project-service@8.70.0(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@6.0.3) - '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) + '@typescript-eslint/types': 8.70.0 debug: 4.4.3(patch_hash=cf37fa96f5df733456b16c82c9e1c9054a92f6216692aa33d5e2e1e208888e37)(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.69.0': + '@typescript-eslint/scope-manager@8.70.0': dependencies: - '@typescript-eslint/types': 8.69.0 - '@typescript-eslint/visitor-keys': 8.69.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 - '@typescript-eslint/tsconfig-utils@8.69.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.69.0 - '@typescript-eslint/typescript-estree': 8.69.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) debug: 4.4.3(patch_hash=cf37fa96f5df733456b16c82c9e1c9054a92f6216692aa33d5e2e1e208888e37)(supports-color@8.1.1) eslint: 10.10.0(jiti@2.7.0)(supports-color@8.1.1) ts-api-utils: 2.5.0(typescript@6.0.3) @@ -5879,14 +5879,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.69.0': {} + '@typescript-eslint/types@8.70.0': {} - '@typescript-eslint/typescript-estree@8.69.0(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.70.0(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.69.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@6.0.3) - '@typescript-eslint/types': 8.69.0 - '@typescript-eslint/visitor-keys': 8.69.0 + '@typescript-eslint/project-service': 8.70.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 debug: 4.4.3(patch_hash=cf37fa96f5df733456b16c82c9e1c9054a92f6216692aa33d5e2e1e208888e37)(supports-color@8.1.1) minimatch: 10.2.6 semver: 7.8.5 @@ -5896,40 +5896,40 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/utils@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1)) - '@typescript-eslint/scope-manager': 8.69.0 - '@typescript-eslint/types': 8.69.0 - '@typescript-eslint/typescript-estree': 8.69.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@8.1.1)(typescript@6.0.3) eslint: 10.10.0(jiti@2.7.0)(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.69.0': + '@typescript-eslint/visitor-keys@8.70.0': dependencies: - '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/types': 8.70.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@6.1.1(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': + '@vitejs/plugin-react@6.1.1(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.11 - ast-v8-to-istanbul: 1.0.5 + ast-v8-to-istanbul: 1.0.6 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 magicast: 0.5.4 - obug: 2.1.4 + obug: 2.2.1 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.11(@types/node@25.9.6)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/expect@4.1.11': dependencies: @@ -5940,13 +5940,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) '@vitest/pretty-format@3.2.7': dependencies: @@ -5984,7 +5984,7 @@ snapshots: '@yarnpkg/lockfile@1.1.0': {} - '@zip.js/zip.js@2.11.2': {} + '@zip.js/zip.js@2.14.0': {} abbrev@4.0.0: {} @@ -6024,7 +6024,7 @@ snapshots: ansi-styles@5.2.0: {} - app-builder-lib@26.15.3(patch_hash=31b6515261c0293b0461ee1fb6771aff53487406edc56e942bc7095b86ac42cf)(dmg-builder@26.16.0)(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1): + app-builder-lib@26.15.3(patch_hash=31b6515261c0293b0461ee1fb6771aff53487406edc56e942bc7095b86ac42cf)(dmg-builder@26.16.1)(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1): dependencies: '@electron/asar': 4.3.0 '@electron/fuses': 1.8.0 @@ -6045,11 +6045,11 @@ snapshots: chromium-pickle-js: 0.2.0 ci-info: 4.3.1 debug: 4.4.3(patch_hash=cf37fa96f5df733456b16c82c9e1c9054a92f6216692aa33d5e2e1e208888e37)(supports-color@8.1.1) - dmg-builder: 26.16.0(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) + dmg-builder: 26.16.1(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) dotenv: 16.6.1 dotenv-expand: 11.0.7 ejs: 3.1.10 - electron-builder-squirrel-windows: 26.15.3(dmg-builder@26.16.0)(supports-color@8.1.1) + electron-builder-squirrel-windows: 26.15.3(dmg-builder@26.16.1)(supports-color@8.1.1) electron-publish: 26.15.3(supports-color@8.1.1) fs-extra: 10.1.0 hosted-git-info: 4.1.0 @@ -6085,14 +6085,14 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-includes@3.1.9: + array-includes@3.2.0: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.24.2 es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 + es-shim-unscopables: 1.1.0 is-string: 1.1.1 math-intrinsics: 1.1.0 @@ -6157,7 +6157,7 @@ snapshots: ast-types-flow@0.0.8: {} - ast-v8-to-istanbul@1.0.5: + ast-v8-to-istanbul@1.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -6226,8 +6226,8 @@ snapshots: dependencies: baseline-browser-mapping: 2.11.21 caniuse-lite: 1.0.30001810 - electron-to-chromium: 1.5.422 - node-releases: 2.0.54 + electron-to-chromium: 1.5.426 + node-releases: 2.0.55 update-browserslist-db: 1.3.2(browserslist@4.28.9) buffer-from@1.1.2: {} @@ -6573,9 +6573,9 @@ snapshots: minimatch: 3.1.5 p-limit: 3.1.0 - dmg-builder@26.16.0(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1): + dmg-builder@26.16.1(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1): dependencies: - app-builder-lib: 26.15.3(patch_hash=31b6515261c0293b0461ee1fb6771aff53487406edc56e942bc7095b86ac42cf)(dmg-builder@26.16.0)(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) + app-builder-lib: 26.15.3(patch_hash=31b6515261c0293b0461ee1fb6771aff53487406edc56e942bc7095b86ac42cf)(dmg-builder@26.16.1)(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) builder-util: 26.16.0(supports-color@8.1.1) fs-extra: 10.1.0 js-yaml: 4.3.2 @@ -6615,23 +6615,23 @@ snapshots: dependencies: jake: 10.9.4 - electron-builder-squirrel-windows@26.15.3(dmg-builder@26.16.0)(supports-color@8.1.1): + electron-builder-squirrel-windows@26.15.3(dmg-builder@26.16.1)(supports-color@8.1.1): dependencies: - app-builder-lib: 26.15.3(patch_hash=31b6515261c0293b0461ee1fb6771aff53487406edc56e942bc7095b86ac42cf)(dmg-builder@26.16.0)(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) + app-builder-lib: 26.15.3(patch_hash=31b6515261c0293b0461ee1fb6771aff53487406edc56e942bc7095b86ac42cf)(dmg-builder@26.16.1)(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) builder-util: 26.15.3(supports-color@8.1.1) electron-winstaller: 5.4.0(supports-color@8.1.1) transitivePeerDependencies: - dmg-builder - supports-color - electron-builder@26.16.0(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1): + electron-builder@26.16.1(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1): dependencies: - app-builder-lib: 26.15.3(patch_hash=31b6515261c0293b0461ee1fb6771aff53487406edc56e942bc7095b86ac42cf)(dmg-builder@26.16.0)(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) + app-builder-lib: 26.15.3(patch_hash=31b6515261c0293b0461ee1fb6771aff53487406edc56e942bc7095b86ac42cf)(dmg-builder@26.16.1)(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) builder-util: 26.16.0(supports-color@8.1.1) builder-util-runtime: 9.7.0(supports-color@8.1.1) chalk: 4.1.2 ci-info: 4.4.0 - dmg-builder: 26.16.0(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) + dmg-builder: 26.16.1(electron-builder-squirrel-windows@26.15.3)(supports-color@8.1.1) fs-extra: 10.1.0 lazy-val: 1.0.5 simple-update-notifier: 2.0.0 @@ -6654,7 +6654,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-to-chromium@1.5.422: {} + electron-to-chromium@1.5.426: {} electron-updater@6.8.9(supports-color@8.1.1): dependencies: @@ -6681,11 +6681,11 @@ snapshots: transitivePeerDependencies: - supports-color - electron@44.2.0(supports-color@8.1.1): + electron@44.3.0(supports-color@8.1.1): dependencies: '@electron-internal/extract-zip': 1.0.5 '@electron/get': 5.1.0(supports-color@8.1.1) - '@types/node': 24.13.3 + '@types/node': 24.13.4 transitivePeerDependencies: - supports-color @@ -6702,7 +6702,7 @@ snapshots: entities@4.5.0: {} - entities@8.0.0: {} + entities@8.1.0: {} env-paths@2.2.1: {} @@ -6873,11 +6873,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) eslint: 10.10.0(jiti@2.7.0)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) transitivePeerDependencies: @@ -6888,10 +6888,10 @@ snapshots: eslint: 10.10.0(jiti@2.7.0)(supports-color@8.1.1) requireindex: 1.1.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 @@ -6899,7 +6899,7 @@ snapshots: doctrine: 2.1.0 eslint: 10.10.0(jiti@2.7.0)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -6911,7 +6911,7 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -6920,7 +6920,7 @@ snapshots: eslint-plugin-jsx-a11y@6.10.2(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: aria-query: 5.3.2 - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 axe-core: 4.13.0 @@ -6955,14 +6955,14 @@ snapshots: '@babel/parser': 7.29.8 eslint: 10.10.0(jiti@2.7.0)(supports-color@8.1.1) hermes-parser: 0.25.1 - zod: 4.5.4 - zod-validation-error: 4.0.2(zod@4.5.4) + zod: 4.6.1 + zod-validation-error: 4.0.2(zod@4.6.1) transitivePeerDependencies: - supports-color eslint-plugin-react@7.37.5(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.findlast: 1.2.5 array.prototype.flatmap: 1.3.3 array.prototype.tosorted: 1.1.4 @@ -7155,14 +7155,14 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 - framer-motion@12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + framer-motion@12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: motion-dom: 12.43.0 motion-utils: 12.39.0 tslib: 2.8.1 optionalDependencies: - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) fs-extra@10.1.0: dependencies: @@ -7300,7 +7300,7 @@ snapshots: dependencies: '@sindresorhus/merge-streams': 4.0.0 fast-glob: 3.3.3 - ignore: 7.0.8 + ignore: 7.0.9 is-path-inside: 4.0.0 slash: 5.1.0 unicorn-magic: 0.4.0 @@ -7404,7 +7404,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.8: {} + ignore@7.0.9: {} immediate@3.0.6: {} @@ -7697,12 +7697,12 @@ snapshots: jsx-ast-utils@3.3.5: dependencies: - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.flat: 1.3.3 object.assign: 4.1.7 object.values: 1.2.1 - jszip@3.10.1: + jszip@3.10.2: dependencies: lie: 3.3.0 pako: 1.0.11 @@ -7886,11 +7886,11 @@ snapshots: dependencies: yallist: 4.0.0 - lucide-react-motion@0.4.0(motion@12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + lucide-react-motion@0.4.0(motion@12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - motion: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) + motion: 12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) lz-string@1.5.0: {} @@ -8189,13 +8189,13 @@ snapshots: motion-utils@12.39.0: {} - motion@12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + motion@12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - framer-motion: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + framer-motion: 12.43.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) tslib: 2.8.1 optionalDependencies: - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) mqtt-packet@9.0.2(supports-color@8.1.1): dependencies: @@ -8281,7 +8281,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.54: {} + node-releases@2.0.55: {} nopt@9.0.0: dependencies: @@ -8338,7 +8338,7 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.2 - obug@2.1.4: {} + obug@2.2.1: {} once@1.4.0: dependencies: @@ -8401,7 +8401,7 @@ snapshots: parse5@8.0.1: dependencies: - entities: 8.0.0 + entities: 8.1.0 patch-package@8.0.1: dependencies: @@ -8552,52 +8552,52 @@ snapshots: quick-lru@5.1.1: {} - react-dom@19.2.8(react@19.2.8): + react-dom@19.3.0(react@19.3.0): dependencies: - react: 19.2.8 - scheduler: 0.27.0 + react: 19.3.0 + scheduler: 0.28.0 - react-i18next@17.0.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3): + react-i18next@17.0.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.7 html-parse-stringify: 4.0.1 i18next: 26.4.2(typescript@6.0.3) - react: 19.2.8 - use-sync-external-store: 1.6.0(react@19.2.8) + react: 19.3.0 + use-sync-external-store: 1.7.0(react@19.3.0) optionalDependencies: - react-dom: 19.2.8(react@19.2.8) + react-dom: 19.3.0(react@19.3.0) typescript: 6.0.3 react-is@16.13.1: {} react-is@17.0.2: {} - react-leaflet-cluster@4.1.3(@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): + react-leaflet-cluster@4.1.3(@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0): dependencies: - '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) leaflet: 1.9.4 leaflet.markercluster: 1.5.3(leaflet@1.9.4) - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - react-leaflet: 5.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + react-leaflet: 5.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) leaflet: 1.9.4 - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) - react-redux@9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1): + react-redux@9.3.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 - react: 19.2.8 - use-sync-external-store: 1.6.0(react@19.2.8) + react: 19.3.0 + use-sync-external-store: 1.7.0(react@19.3.0) optionalDependencies: - '@types/react': 19.2.18 + '@types/react': 19.3.0 redux: 5.0.1 - react@19.2.8: {} + react@19.3.0: {} read-binary-file-arch@1.0.6(supports-color@8.1.1): dependencies: @@ -8629,21 +8629,21 @@ snapshots: process: 0.11.10 string_decoder: 1.3.0 - recharts@3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@17.0.2)(react@19.2.8)(redux@5.0.1): + recharts@3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@17.0.2)(react@19.3.0)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1))(react@19.2.8) + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1))(react@19.3.0) clsx: 2.1.1 decimal.js-light: 2.5.1 es-toolkit: 1.52.0 eventemitter3: 5.0.4 immer: 11.1.18 - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) react-is: 17.0.2 - react-redux: 9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1) + react-redux: 9.3.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1) reselect: 5.2.0 tiny-invariant: 1.3.3 - use-sync-external-store: 1.6.0(react@19.2.8) + use-sync-external-store: 1.7.0(react@19.3.0) victory-vendor: 37.3.6 transitivePeerDependencies: - '@types/react' @@ -8737,26 +8737,26 @@ snapshots: sprintf-js: 1.1.3 optional: true - rolldown@1.2.7: + rolldown@1.2.8: dependencies: - '@oxc-project/types': 0.148.0 + '@oxc-project/types': 0.149.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.7 - '@rolldown/binding-android-arm64': 1.2.7 - '@rolldown/binding-darwin-arm64': 1.2.7 - '@rolldown/binding-darwin-x64': 1.2.7 - '@rolldown/binding-freebsd-x64': 1.2.7 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.7 - '@rolldown/binding-linux-arm64-gnu': 1.2.7 - '@rolldown/binding-linux-arm64-musl': 1.2.7 - '@rolldown/binding-linux-ppc64-gnu': 1.2.7 - '@rolldown/binding-linux-s390x-gnu': 1.2.7 - '@rolldown/binding-linux-x64-gnu': 1.2.7 - '@rolldown/binding-linux-x64-musl': 1.2.7 - '@rolldown/binding-openharmony-arm64': 1.2.7 - '@rolldown/binding-win32-arm64-msvc': 1.2.7 - '@rolldown/binding-win32-x64-msvc': 1.2.7 + '@rolldown/binding-android-arm-eabi': 1.2.8 + '@rolldown/binding-android-arm64': 1.2.8 + '@rolldown/binding-darwin-arm64': 1.2.8 + '@rolldown/binding-darwin-x64': 1.2.8 + '@rolldown/binding-freebsd-x64': 1.2.8 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.8 + '@rolldown/binding-linux-arm64-gnu': 1.2.8 + '@rolldown/binding-linux-arm64-musl': 1.2.8 + '@rolldown/binding-linux-ppc64-gnu': 1.2.8 + '@rolldown/binding-linux-s390x-gnu': 1.2.8 + '@rolldown/binding-linux-x64-gnu': 1.2.8 + '@rolldown/binding-linux-x64-musl': 1.2.8 + '@rolldown/binding-openharmony-arm64': 1.2.8 + '@rolldown/binding-win32-arm64-msvc': 1.2.8 + '@rolldown/binding-win32-x64-msvc': 1.2.8 run-parallel@1.2.0: dependencies: @@ -8803,7 +8803,7 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.27.0: {} + scheduler@0.28.0: {} semver-compare@1.0.0: optional: true @@ -9096,7 +9096,7 @@ snapshots: dependencies: '@pkgr/core': 0.3.6 - systeminformation@5.33.8: {} + systeminformation@5.33.10: {} tailwindcss@4.3.3: {} @@ -9228,12 +9228,12 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): + typescript-eslint@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.69.0(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) eslint: 10.10.0(jiti@2.7.0)(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: @@ -9287,9 +9287,9 @@ snapshots: node-gyp-build: 4.8.4 optional: true - use-sync-external-store@1.6.0(react@19.2.8): + use-sync-external-store@1.7.0(react@19.3.0): dependencies: - react: 19.2.8 + react: 19.3.0 utf8-byte-length@1.0.5: {} @@ -9312,15 +9312,15 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0): + vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 postcss: 8.5.28 - rolldown: 1.2.7 + rolldown: 1.2.8 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 25.9.6 esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 @@ -9332,12 +9332,12 @@ snapshots: axe-core: 4.13.0 chalk: 5.6.2 lodash-es: 4.18.1 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.11(@types/node@25.9.6)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) - vitest@4.1.11(@types/node@25.9.5)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.11(@types/node@25.9.6)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -9346,7 +9346,7 @@ snapshots: es-module-lexer: 2.3.2 expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.4 + obug: 2.2.1 pathe: 2.0.3 picomatch: 4.0.7 std-env: 4.2.0 @@ -9354,10 +9354,10 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.2(@types/node@25.9.6)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.9.5 + '@types/node': 25.9.6 '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) jsdom: 29.1.1(@noble/hashes@2.4.0) transitivePeerDependencies: @@ -9553,15 +9553,15 @@ snapshots: yocto-queue@0.1.0: {} - zod-validation-error@4.0.2(zod@4.5.4): + zod-validation-error@4.0.2(zod@4.6.1): dependencies: - zod: 4.5.4 + zod: 4.6.1 - zod@4.5.4: {} + zod@4.6.1: {} - zustand@5.0.15(@types/react@19.2.18)(immer@11.1.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)): + zustand@5.0.15(@types/react@19.3.0)(immer@11.1.18)(react@19.3.0)(use-sync-external-store@1.7.0(react@19.3.0)): optionalDependencies: - '@types/react': 19.2.18 + '@types/react': 19.3.0 immer: 11.1.18 - react: 19.2.8 - use-sync-external-store: 1.6.0(react@19.2.8) + react: 19.3.0 + use-sync-external-store: 1.7.0(react@19.3.0) diff --git a/reticulum-sidecar/Cargo.lock b/reticulum-sidecar/Cargo.lock index 2106459a5..bfd69fe52 100644 --- a/reticulum-sidecar/Cargo.lock +++ b/reticulum-sidecar/Cargo.lock @@ -1662,6 +1662,7 @@ version = "0.1.0" dependencies = [ "bytes", "hex", + "libc", "rmpv", "rns-crypto", "rns-identity", From 3cf184a6cba3e2de69f3af5d67dacf22bec471fe Mon Sep 17 00:00:00 2001 From: Joey Stanford Date: Fri, 11 Sep 2026 06:05:44 -0600 Subject: [PATCH 5/6] fix: unblock eslint empty-object type and cover overlay apply paths typescript-eslint 8.70 flags NonNullable in useSendMessage; add fixture tests for the new Ratspeak apply scripts and an execution-based check_ratspeak_patches missing-overlay warning. --- ...pply-rsLXMF-file-attachments-list.test.mjs | 217 ++++++++++++++++++ ...ticulum-reply-file-query-metadata.test.mjs | 217 ++++++++++++++++++ scripts/update.sh | 8 + scripts/update.test.mjs | 45 ++++ src/renderer/hooks/useSendMessage.ts | 4 +- 5 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 scripts/apply-rsLXMF-file-attachments-list.test.mjs create mode 100644 scripts/apply-rsReticulum-reply-file-query-metadata.test.mjs diff --git a/scripts/apply-rsLXMF-file-attachments-list.test.mjs b/scripts/apply-rsLXMF-file-attachments-list.test.mjs new file mode 100644 index 000000000..685bd9b68 --- /dev/null +++ b/scripts/apply-rsLXMF-file-attachments-list.test.mjs @@ -0,0 +1,217 @@ +import { spawnSync } from 'node:child_process'; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { afterEach, describe, expect, it } from 'vitest'; + +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(SCRIPT_DIR, '..'); +const APPLY_SCRIPT = path.join(SCRIPT_DIR, 'apply-rsLXMF-file-attachments-list.sh'); +const HELPER_SCRIPT = path.join(SCRIPT_DIR, 'lib/apply-ratspeak-overlay.sh'); +const PATCH_FILE = path.join( + REPO_ROOT, + 'reticulum-sidecar/patches/rsLXMF-file-attachments-list.patch', +); + +const GIT_TEST_ENV = { + ...process.env, + GIT_CONFIG_NOSYSTEM: '1', + GIT_CONFIG_GLOBAL: '/dev/null', +}; + +/** @type {string[]} */ +const temps = []; + +function git(cwd, args) { + return spawnSync('git', args, { + cwd, + encoding: 'utf8', + env: GIT_TEST_ENV, + }); +} + +function parseUnifiedHunkHeader(line) { + if (!line.startsWith('@@ -')) { + return null; + } + const close = line.indexOf(' @@', 4); + if (close < 0) { + return null; + } + const [oldSpec, newSpec] = line.slice(4, close).split(' +'); + if (!oldSpec || !newSpec) { + return null; + } + const oldStart = Number(oldSpec.split(',')[0]); + const newStart = Number(newSpec.split(',')[0]); + if (!Number.isInteger(oldStart) || !Number.isInteger(newStart)) { + return null; + } + return { oldStart, newStart }; +} + +function materializePatchFiles(patchText, side) { + /** @type {Map} */ + const files = new Map(); + const patchLines = patchText.replace(/\n$/, '').split('\n'); + let i = 0; + /** @type {string | null} */ + let currentPath = null; + + while (i < patchLines.length) { + const line = patchLines[i]; + if (line.startsWith('diff --git ')) { + const match = line.match(/^diff --git a\/(.+) b\/(.+)$/); + currentPath = match ? match[2] : null; + if (currentPath && !files.has(currentPath)) { + files.set(currentPath, []); + } + i += 1; + continue; + } + const hunk = parseUnifiedHunkHeader(line); + if (hunk && currentPath) { + const lines = files.get(currentPath) ?? []; + const start = side === 'old' ? hunk.oldStart : hunk.newStart; + while (lines.length < start - 1) { + lines.push(`// overlay-fixture-pad ${lines.length + 1}`); + } + i += 1; + while ( + i < patchLines.length && + !patchLines[i].startsWith('@@ ') && + !patchLines[i].startsWith('diff --git ') + ) { + const hunkLine = patchLines[i]; + if (hunkLine.startsWith('\\')) { + i += 1; + continue; + } + const tag = hunkLine[0]; + const body = hunkLine.slice(1); + if (tag === ' ') { + lines.push(body); + } else if (tag === '-' && side === 'old') { + lines.push(body); + } else if (tag === '+' && side === 'new') { + lines.push(body); + } + i += 1; + } + files.set(currentPath, lines); + continue; + } + i += 1; + } + + /** @type {Map} */ + const out = new Map(); + for (const [rel, lines] of files) { + out.set(rel, `${lines.join('\n')}\n`); + } + return out; +} + +function makeFakeRsLxmfFromFiles(files) { + const root = mkdtempSync(path.join(os.tmpdir(), 'mesh-file-attachments-lxmf-')); + temps.push(root); + for (const [rel, content] of files) { + const abs = path.join(root, rel); + mkdirSync(path.dirname(abs), { recursive: true }); + writeFileSync(abs, content); + } + const gitInit = git(root, ['init']); + expect(gitInit.status).toBe(0); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + git(root, ['add', '.']); + const commit = git(root, ['commit', '-m', 'init']); + expect(commit.status).toBe(0); + return root; +} + +function runApply(lxmfDir, applyScript = APPLY_SCRIPT) { + return spawnSync('bash', [applyScript], { + cwd: REPO_ROOT, + encoding: 'utf8', + env: { ...GIT_TEST_ENV, RS_LXMF_DIR: lxmfDir }, + }); +} + +/** Copy apply script + helper into a fake repo root with no patch file. */ +function prepareMissingPatchScriptTree() { + const work = mkdtempSync(path.join(os.tmpdir(), 'mesh-file-attachments-missing-patch-')); + temps.push(work); + const scriptsDir = path.join(work, 'scripts'); + mkdirSync(path.join(scriptsDir, 'lib'), { recursive: true }); + mkdirSync(path.join(work, 'reticulum-sidecar/patches'), { recursive: true }); + const applyCopy = path.join(scriptsDir, 'apply-rsLXMF-file-attachments-list.sh'); + copyFileSync(APPLY_SCRIPT, applyCopy); + copyFileSync(HELPER_SCRIPT, path.join(scriptsDir, 'lib/apply-ratspeak-overlay.sh')); + return applyCopy; +} + +afterEach(() => { + while (temps.length > 0) { + const dir = temps.pop(); + rmSync(dir, { recursive: true, force: true }); + } +}); + +describe('apply-rsLXMF-file-attachments-list.sh', () => { + it('fails when rsLXMF checkout is missing', () => { + const missing = path.join( + mkdtempSync(path.join(os.tmpdir(), 'mesh-file-attachments-missing-checkout-')), + 'rsLXMF', + ); + temps.push(path.dirname(missing)); + const result = runApply(missing); + expect(result.status).not.toBe(0); + expect(result.stderr).toMatch(/rsLXMF not found/); + }); + + it('fails when the overlay patch file is missing', () => { + const files = materializePatchFiles(readFileSync(PATCH_FILE, 'utf8'), 'old'); + const lxmf = makeFakeRsLxmfFromFiles(files); + const applyCopy = prepareMissingPatchScriptTree(); + const result = runApply(lxmf, applyCopy); + expect(result.status).not.toBe(0); + expect(result.stderr).toMatch(/patch not found/); + }); + + it('is a no-op when multi-file attachment markers are already present', () => { + const lxmf = makeFakeRsLxmfFromFiles( + new Map([ + [ + 'crates/lxmf-core/src/message.rs', + `impl LxMessage { + pub fn set_file_attachments_field(&mut self, attachments: &[(&str, &[u8])]) -> Result<(), MessageError> { + Ok(()) + } + pub fn file_attachments(&self) -> Result, MessageError> { + Ok(Vec::new()) + } +} +`, + ], + ]), + ); + const result = runApply(lxmf); + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toMatch(/already present/); + }); + + it('applies on a clean checkout matching the overlay context', () => { + const files = materializePatchFiles(readFileSync(PATCH_FILE, 'utf8'), 'old'); + expect(files.has('crates/lxmf-core/src/message.rs')).toBe(true); + const lxmf = makeFakeRsLxmfFromFiles(files); + const result = runApply(lxmf); + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toMatch(/applied .*rsLXMF-file-attachments-list\.patch/); + const messageRs = readFileSync(path.join(lxmf, 'crates/lxmf-core/src/message.rs'), 'utf8'); + expect(messageRs).toContain('fn set_file_attachments_field('); + expect(messageRs).toContain('fn file_attachments('); + }); +}); diff --git a/scripts/apply-rsReticulum-reply-file-query-metadata.test.mjs b/scripts/apply-rsReticulum-reply-file-query-metadata.test.mjs new file mode 100644 index 000000000..d4d070ddd --- /dev/null +++ b/scripts/apply-rsReticulum-reply-file-query-metadata.test.mjs @@ -0,0 +1,217 @@ +import { spawnSync } from 'node:child_process'; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { afterEach, describe, expect, it } from 'vitest'; + +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(SCRIPT_DIR, '..'); +const APPLY_SCRIPT = path.join(SCRIPT_DIR, 'apply-rsReticulum-reply-file-query-metadata.sh'); +const HELPER_SCRIPT = path.join(SCRIPT_DIR, 'lib/apply-ratspeak-overlay.sh'); +const PATCH_FILE = path.join( + REPO_ROOT, + 'reticulum-sidecar/patches/rsReticulum-reply-file-query-metadata.patch', +); + +const GIT_TEST_ENV = { + ...process.env, + GIT_CONFIG_NOSYSTEM: '1', + GIT_CONFIG_GLOBAL: '/dev/null', +}; + +/** @type {string[]} */ +const temps = []; + +function git(cwd, args) { + return spawnSync('git', args, { + cwd, + encoding: 'utf8', + env: GIT_TEST_ENV, + }); +} + +function parseUnifiedHunkHeader(line) { + if (!line.startsWith('@@ -')) { + return null; + } + const close = line.indexOf(' @@', 4); + if (close < 0) { + return null; + } + const [oldSpec, newSpec] = line.slice(4, close).split(' +'); + if (!oldSpec || !newSpec) { + return null; + } + const oldStart = Number(oldSpec.split(',')[0]); + const newStart = Number(newSpec.split(',')[0]); + if (!Number.isInteger(oldStart) || !Number.isInteger(newStart)) { + return null; + } + return { oldStart, newStart }; +} + +function materializePatchFiles(patchText, side) { + /** @type {Map} */ + const files = new Map(); + const patchLines = patchText.replace(/\n$/, '').split('\n'); + let i = 0; + /** @type {string | null} */ + let currentPath = null; + + while (i < patchLines.length) { + const line = patchLines[i]; + if (line.startsWith('diff --git ')) { + const match = line.match(/^diff --git a\/(.+) b\/(.+)$/); + currentPath = match ? match[2] : null; + if (currentPath && !files.has(currentPath)) { + files.set(currentPath, []); + } + i += 1; + continue; + } + const hunk = parseUnifiedHunkHeader(line); + if (hunk && currentPath) { + const lines = files.get(currentPath) ?? []; + const start = side === 'old' ? hunk.oldStart : hunk.newStart; + while (lines.length < start - 1) { + lines.push(`// overlay-fixture-pad ${lines.length + 1}`); + } + i += 1; + while ( + i < patchLines.length && + !patchLines[i].startsWith('@@ ') && + !patchLines[i].startsWith('diff --git ') + ) { + const hunkLine = patchLines[i]; + if (hunkLine.startsWith('\\')) { + i += 1; + continue; + } + const tag = hunkLine[0]; + const body = hunkLine.slice(1); + if (tag === ' ') { + lines.push(body); + } else if (tag === '-' && side === 'old') { + lines.push(body); + } else if (tag === '+' && side === 'new') { + lines.push(body); + } + i += 1; + } + files.set(currentPath, lines); + continue; + } + i += 1; + } + + /** @type {Map} */ + const out = new Map(); + for (const [rel, lines] of files) { + out.set(rel, `${lines.join('\n')}\n`); + } + return out; +} + +function makeFakeRsReticulumFromFiles(files) { + const root = mkdtempSync(path.join(os.tmpdir(), 'mesh-reply-file-rns-')); + temps.push(root); + for (const [rel, content] of files) { + const abs = path.join(root, rel); + mkdirSync(path.dirname(abs), { recursive: true }); + writeFileSync(abs, content); + } + const gitInit = git(root, ['init']); + expect(gitInit.status).toBe(0); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + git(root, ['add', '.']); + const commit = git(root, ['commit', '-m', 'init']); + expect(commit.status).toBe(0); + return root; +} + +function runApply(rnsDir, applyScript = APPLY_SCRIPT, extraEnv = {}) { + return spawnSync('bash', [applyScript], { + cwd: REPO_ROOT, + encoding: 'utf8', + env: { ...GIT_TEST_ENV, RS_RETICULUM_DIR: rnsDir, ...extraEnv }, + }); +} + +/** Copy apply script + helper into a fake repo root with no patch file. */ +function prepareMissingPatchScriptTree() { + const work = mkdtempSync(path.join(os.tmpdir(), 'mesh-reply-file-missing-patch-')); + temps.push(work); + const scriptsDir = path.join(work, 'scripts'); + mkdirSync(path.join(scriptsDir, 'lib'), { recursive: true }); + mkdirSync(path.join(work, 'reticulum-sidecar/patches'), { recursive: true }); + const applyCopy = path.join(scriptsDir, 'apply-rsReticulum-reply-file-query-metadata.sh'); + copyFileSync(APPLY_SCRIPT, applyCopy); + copyFileSync(HELPER_SCRIPT, path.join(scriptsDir, 'lib/apply-ratspeak-overlay.sh')); + return applyCopy; +} + +afterEach(() => { + while (temps.length > 0) { + const dir = temps.pop(); + rmSync(dir, { recursive: true, force: true }); + } +}); + +describe('apply-rsReticulum-reply-file-query-metadata.sh', () => { + it('fails when rsReticulum checkout is missing', () => { + const missing = path.join( + mkdtempSync(path.join(os.tmpdir(), 'mesh-reply-file-missing-checkout-')), + 'rsReticulum', + ); + temps.push(path.dirname(missing)); + const result = runApply(missing); + expect(result.status).not.toBe(0); + expect(result.stderr).toMatch(/rsReticulum not found/); + }); + + it('fails when the overlay patch file is missing', () => { + const files = materializePatchFiles(readFileSync(PATCH_FILE, 'utf8'), 'old'); + const rns = makeFakeRsReticulumFromFiles(files); + const applyCopy = prepareMissingPatchScriptTree(); + const result = runApply(rns, applyCopy); + expect(result.status).not.toBe(0); + expect(result.stderr).toMatch(/patch not found/); + }); + + it('is a no-op when ReplyFile overlay markers are already present', () => { + const rns = makeFakeRsReticulumFromFiles( + new Map([ + [ + 'crates/rns-runtime/src/link_manager.rs', + `pub enum RequestOutcome { + Reply(Vec), + ReplyFile { path: PathBuf, metadata: Option> }, +} +fn pack_file_name_metadata(name: &str) -> Vec { vec![] } +`, + ], + ]), + ); + const result = runApply(rns); + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toMatch(/already present/); + }); + + it('applies on a clean checkout matching the overlay context', () => { + const files = materializePatchFiles(readFileSync(PATCH_FILE, 'utf8'), 'old'); + expect(files.has('crates/rns-runtime/src/link_manager.rs')).toBe(true); + const rns = makeFakeRsReticulumFromFiles(files); + const result = runApply(rns); + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toMatch(/applied .*rsReticulum-reply-file-query-metadata\.patch/); + const linkManager = readFileSync( + path.join(rns, 'crates/rns-runtime/src/link_manager.rs'), + 'utf8', + ); + expect(linkManager).toMatch(/ReplyFile\s*\{/); + expect(linkManager).toContain('fn pack_file_name_metadata('); + }); +}); diff --git a/scripts/update.sh b/scripts/update.sh index 651e4ad0d..ed7583aef 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -741,6 +741,14 @@ if [ "${UPDATE_SH_TEST_HOOK:-}" = 'upstream-check-only' ]; then exit 0 fi +# Test hook: exercise check_ratspeak_patches (fake gh via PATH; cwd may supply patches/). +if [ "${UPDATE_SH_TEST_HOOK:-}" = 'ratspeak-patches-only' ]; then + HAS_WARNING=0 + check_ratspeak_patches + printf 'HAS_WARNING=%s\n' "${HAS_WARNING}" + 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 diff --git a/scripts/update.test.mjs b/scripts/update.test.mjs index 85972934a..ce73bc62e 100644 --- a/scripts/update.test.mjs +++ b/scripts/update.test.mjs @@ -141,6 +141,51 @@ describe('update.sh Reticulum stack functionality check', () => { expect(upstreamCall).toBeGreaterThan(patchesCall); }); + it('warns when an open upstream PR has no local overlay patch', () => { + const work = mkdtempSync(path.join(os.tmpdir(), 'mesh-update-patches-')); + tempDirs.push(work); + mkdirSync(path.join(work, 'reticulum-sidecar/patches'), { recursive: true }); + // Intentionally omit tracked overlays so open-PR + missing-patch fires. + const binDir = path.join(work, 'bin'); + mkdirSync(binDir, { recursive: true }); + const ghPath = path.join(binDir, 'gh'); + writeFileSync( + ghPath, + `#!/usr/bin/env bash +set -euo pipefail +if [[ "\${1:-}" != "api" ]]; then + echo "unexpected gh args: $*" >&2 + exit 1 +fi +path="\${2:-}" +if [[ "$path" == repos/*/pulls/* ]]; then + printf '%s' '{"state":"open","merged":false}' + exit 0 +fi +echo "unexpected gh api path: $path" >&2 +exit 1 +`, + 'utf8', + ); + chmodSync(ghPath, 0o755); + const result = runUpdate( + [], + { + UPDATE_SH_TEST_HOOK: 'ratspeak-patches-only', + PATH: `${binDir}:${process.env.PATH ?? ''}`, + }, + work, + ); + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toContain('HAS_WARNING=1'); + expect(result.stdout).toMatch( + /ratspeak\/rsReticulum#26 open but rsReticulum-reply-file-query-metadata\.patch missing/, + ); + expect(result.stdout).toMatch( + /ratspeak\/rsLXMF#7 open but rsLXMF-file-attachments-list\.patch missing/, + ); + }); + it('wires check_ratspeak_upstream after overlay PR checks', () => { expect(updateScript).toContain('check_ratspeak_upstream()'); expect(updateScript).toContain('RATSPEAK_RELEASE_WATCH_ENTRIES'); diff --git a/src/renderer/hooks/useSendMessage.ts b/src/renderer/hooks/useSendMessage.ts index 8f60c7d96..13f038ac5 100644 --- a/src/renderer/hooks/useSendMessage.ts +++ b/src/renderer/hooks/useSendMessage.ts @@ -297,8 +297,10 @@ export function useSendMessage( return; } + // getHandle() is `unknown`; NonNullable is `unknown & {}` and trips + // @typescript-eslint/no-generated-empty-object-type under typescript-eslint 8.70+. const finishSend = ( - sendHandle: NonNullable, + sendHandle: unknown, opts?: { trackForOpenHopLiveWindow?: boolean }, ): void => { const sendPromise = identity.protocol.sendMessage(sendHandle, { From 759dbe44add50a2a92af521e88ecb401bdc1301e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 11 Sep 2026 12:20:10 +0000 Subject: [PATCH 6/6] fix(update): warn on missing Ratspeak overlay when PR state unknown check_ratspeak_patches treated unknown GitHub PR lookups as soft diagnostics even when the tracked .patch was absent, so ratspeak-patches-only could report HAS_WARNING=0 with no network/gh. Warn when the overlay is missing. --- scripts/update.sh | 11 ++++++++++- scripts/update.test.mjs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/scripts/update.sh b/scripts/update.sh index ed7583aef..b06ac302a 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -365,7 +365,16 @@ check_ratspeak_patches() { HAS_WARNING=1 ;; *) - echo " ${label}: could not query ${repo}#${pr} (install gh or check network) — ${url}" + # Unknown PR state (gh/network unavailable). Still warn when the tracked + # overlay file is missing so ratspeak-patches-only cannot report clean. + if [ "${patch_present}" -eq 0 ]; then + warn_box "${label} (Ratspeak overlay)" "patch absent" "PR state unknown" "${url}" + echo " ${label}: ${patch_base} missing and could not query ${repo}#${pr} — restore overlay or verify sunset." + has_ratspeak_warning=1 + HAS_WARNING=1 + else + echo " ${label}: could not query ${repo}#${pr} (install gh or check network) — ${url}" + fi ;; esac done diff --git a/scripts/update.test.mjs b/scripts/update.test.mjs index ce73bc62e..ef238969d 100644 --- a/scripts/update.test.mjs +++ b/scripts/update.test.mjs @@ -186,6 +186,47 @@ exit 1 ); }); + it('warns when PR state is unknown and a tracked overlay patch is missing', () => { + const work = mkdtempSync(path.join(os.tmpdir(), 'mesh-update-patches-unknown-')); + tempDirs.push(work); + mkdirSync(path.join(work, 'reticulum-sidecar/patches'), { recursive: true }); + // Intentionally omit tracked overlays; fake gh returns non-PR JSON so state=unknown. + const binDir = path.join(work, 'bin'); + mkdirSync(binDir, { recursive: true }); + const ghPath = path.join(binDir, 'gh'); + writeFileSync( + ghPath, + `#!/usr/bin/env bash +set -euo pipefail +if [[ "\${1:-}" != "api" ]]; then + echo "unexpected gh args: $*" >&2 + exit 1 +fi +# Empty / malformed body → github_pr_state prints unknown. +printf '%s' '{}' +exit 0 +`, + 'utf8', + ); + chmodSync(ghPath, 0o755); + const result = runUpdate( + [], + { + UPDATE_SH_TEST_HOOK: 'ratspeak-patches-only', + PATH: `${binDir}:${process.env.PATH ?? ''}`, + }, + work, + ); + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toContain('HAS_WARNING=1'); + expect(result.stdout).toMatch( + /rsReticulum-reply-file-query-metadata\.patch missing and could not query ratspeak\/rsReticulum#26/, + ); + expect(result.stdout).toMatch( + /rsLXMF-file-attachments-list\.patch missing and could not query ratspeak\/rsLXMF#7/, + ); + }); + it('wires check_ratspeak_upstream after overlay PR checks', () => { expect(updateScript).toContain('check_ratspeak_upstream()'); expect(updateScript).toContain('RATSPEAK_RELEASE_WATCH_ENTRIES');