Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`).
- **Retention:** App defaults Reticulum destination age/count pruning to 30 days / 10,000 destinations (favorites preserved; count max 50,000); Reticulum message retention independently enabled at 4,000. RRC room history retention independently enabled by default at **10,000** messages (30-day age prune) via `rrcMessageRetention*` settings and `db:pruneRrcMessagesByCount` / `db:pruneRrcMessagesByAge`.
- **Self label / header:** `reticulumSelfNodeLabel.ts` (`resolveReticulumSelfHeaderLabel` — Network display name in app header)
- **Nomad errors:** `lib/nomad/nomadPageErrorHumanize.ts` (sidecar error codes → i18n); LinkClient Nomad overlay in `reticulum-sidecar/patches/`
- **LXST voice:** `hasLxstVoice` gates Call buttons (Peers + Chat DM). Session helpers in `reticulumVoiceSession.ts` (dial/answer/hangup + mic PCM); UI store `reticulumVoiceStore.ts`; overlay `ReticulumVoiceOverlay` (App mount). Dedicated IPC `reticulum:voiceSendAudio` + `reticulum:onVoiceAudio` (`/ws/voice`); control via `electronAPI.reticulum.voice.*`. Runtime WS: `voice.update` / `voice.incoming` / `voice.stats` / `voice.terminated` / `voice.error` (errors should carry `link_id` when known; match by link/generation/remote). **Establish-only media:** Answer warms AudioContext; mic capture/TX starts only after `established`; sidecar soft-drops pre-establish PCM (`not_established`). Outbound progress tones: dial → peer DTMF fold → UK double-ring (`reticulumVoiceCallTones.ts` / `reticulumVoiceOutcome.ts` / `reticulumVoiceFeedback.ts`); media-start coalesces by `callGeneration` to avoid Answer mic thrash. Terminal reasons: treat sidecar `established`/`terminated` as completed (not fail).
- **LXST voice:** `hasLxstVoice` gates Call buttons (Peers + Chat DM). Session helpers in `reticulumVoiceSession.ts` (dial/answer/hangup + mic PCM); UI store `reticulumVoiceStore.ts`; overlay `ReticulumVoiceOverlay` (App mount). Dedicated IPC `reticulum:voiceSendAudio` + push channel `reticulum:voiceAudio` (`/ws/voice`; preload `onVoiceAudio`); control via `electronAPI.reticulum.voice.*`. Runtime WS: `voice.update` / `voice.incoming` / `voice.stats` / `voice.terminated` / `voice.error` (errors should carry `link_id` when known; match by link/generation/remote). **Establish-only media:** Answer warms AudioContext; mic capture/TX starts only after `established`; sidecar soft-drops pre-establish PCM (`not_established`). Outbound progress tones: dial → peer DTMF fold → UK double-ring (`reticulumVoiceCallTones.ts` / `reticulumVoiceOutcome.ts` / `reticulumVoiceFeedback.ts`); media-start coalesces by `callGeneration` to avoid Answer mic thrash. Terminal reasons: treat sidecar `established`/`terminated` as completed (not fail).
- **LRGP games:** `hasLrgpGames` gates Games tab + Challenge (Peers / Chat DM). Sidecar `games_session` + `LrgpStore`; companion `games_outbound.db` persists last envelope + `delivery_state` (LXMF outbound bridge → session chips / Resend). Dedicated IPC `electronAPI.reticulum.games.*` / `reticulum:games*` (proxy rejects `/api/v1/games/*`); WS `games.update` / `games.action_result`. Parity: [docs/reticulum-games-parity.md](docs/reticulum-games-parity.md).
- **Gating:** `hasReticulumDiscoveryMap` (Map tab); `hasReticulumRemotePanel` / `hasRncpTransfer` (Remote tab + Chat DM rncp); `hasRrcPanel` (RRC tab); `hasLxstVoice` (LXST Call); `hasLrgpGames` (Games); `hasReticulumInterfaceConfig` / `hasReticulumNetworkPanel` / `ProtocolCapabilities`
- **rnsh/rncp:** sidecar `stack/{rnsh_session,rncp_transfer,path_speed,link_task}.rs` + HTTP `/api/v1/rnsh/*`, `/api/v1/rncp/*`, `/api/v1/remote/*`; typed `electronAPI.reticulum.rnsh|rncp|remote`; picker-gated send/fetch paths in `reticulum-remote-paths.ts`; LXMF enable-request sentinel `mesh-client:request-rncp-receive:v1` (`rncpRequestEnable.ts`); peer reply `mesh-client:rncp-receive-dest:v1:<hash>` autofills via `applyRncpReceiveDestShare` (prefer pending from `markRncpReceiveDestSharePending` / `sendRncpRequestEnable`; still apply without pending for older peers); enable-request modal + dest-share side effects deduped by LXMF `message_hash` (`rncpLxmfControlSideEffectDedup`) so catch-up cannot re-fire; already-listening auto-share is once per peer per request-enable cooldown; inbound listener config persists (`rncp_listener_*` in `mesh_client_stack.json`) and restores on live stack start
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Thank you for your interest in contributing.

See [Test harness setup and local quality checks](docs/development-environment.md#4-test-harness-setup-and-local-quality-checks) for Vitest projects, pre-PR commands, and browser dev stub behavior.

- Renderer: jsdom (`src/renderer/**/*.test.{ts,tsx}`). Main: node (`src/main/**/*.test.ts`).
- **Reticulum sidecar (Rust):** when editing `reticulum-sidecar/**`, run `pnpm run reticulum:sidecar:clippy:full` before PR; CI enforces line coverage in `tests.yaml` when sidecar paths change (see [development-environment.md](development-environment.md#lint-and-coverage-sidecar)).
- Renderer: jsdom (`src/renderer/**/*.test.{ts,tsx}`). Main (node project): `src/main/**/*.test.ts`, plus `src/shared/**`, `src/preload/**`, `src/architecture/**`, `scripts/**/*.test.mjs`, and `vitest.harness.test.ts` (see `vitest.config.mts`).
- **Reticulum sidecar (Rust):** when editing `reticulum-sidecar/**`, run `pnpm run reticulum:sidecar:clippy:full` before PR; CI enforces line coverage in `tests.yaml` when sidecar paths change (see [docs/development-environment.md](docs/development-environment.md#lint-and-coverage-sidecar)).
- Mock console before spying logged errors (e.g. `vi.spyOn(console, 'warn').mockImplementation(() => {})`; use `beforeEach` when shared).
- Update `src/main/index.contract.test.ts` when CSP, build config, IPC limits, or log filters change.
- Accessibility: vitest-axe in component tests; see **Accessibility / axe** in [AGENTS.md](AGENTS.md#5-testing).
Expand Down
2 changes: 1 addition & 1 deletion docs/accessibility-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This is a living document. Check items against VoiceOver (macOS), NVDA (Windows)
## Screen Reader Compatibility

- [ ] App title announced on launch
- [ ] Tab labels (Chat, Nodes, Config…) read correctly
- [ ] Tab labels (Connection, Chat, Nodes, Radio, …) read correctly
- [x] Connection status changes announced (`aria-live="polite"`) — device status in `App.tsx` header (`role="status" aria-live="polite"`); MQTT/TAK indicators still optional follow-up
- [ ] Modal open/close announced as "dialog"
- [x] Confirmation dialogs announced as "alert dialog" — `ConfirmModal` uses `role="alertdialog"` + `aria-describedby`
Expand Down
26 changes: 13 additions & 13 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,19 +468,19 @@ Sidecar APIs: `GET /api/v1/config/audit`, `POST /api/v1/config/repair` (see [`re

For contributors who want to modify or extend the diagnostics system:

| File | Purpose |
| ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| [`src/renderer/stores/diagnosticsStore.ts`](src/renderer/stores/diagnosticsStore.ts) | Zustand store: anomaly state, persistence, MQTT ignore sets, foreign LoRa records |
| [`src/renderer/lib/diagnostics/RoutingDiagnosticEngine.ts`](src/renderer/lib/diagnostics/RoutingDiagnosticEngine.ts) | Hop anomaly detection (hop_goblin, bad_route, impossible_hop, route_flapping) |
| [`src/renderer/lib/diagnostics/RFDiagnosticEngine.ts`](src/renderer/lib/diagnostics/RFDiagnosticEngine.ts) | RF signal analysis (connected node + remote node findings) |
| [`src/renderer/lib/diagnostics/diagnosticRows.ts`](src/renderer/lib/diagnostics/diagnosticRows.ts) | Row merge/prune utilities, `filterDiagnosticRowsForProtocol`, default max-age values |
| [`src/renderer/lib/foreignLoraDetection.ts`](src/renderer/lib/foreignLoraDetection.ts) | Foreign LoRa packet classification, Reticulum overhear heuristic, proximity scoring |
| [`src/renderer/components/DiagnosticsPanel.tsx`](src/renderer/components/DiagnosticsPanel.tsx) | Diagnostics tab UI: health band + counts, anomaly table, foreign LoRa tables, settings |
| [`src/renderer/components/ReticulumDiagnosticsSection.tsx`](src/renderer/components/ReticulumDiagnosticsSection.tsx) | Reticulum config audit table + repair actions |
| [`src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts`](src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts) | Reticulum-native diagnostic rows (interfaces, audit merge) |
| [`src/renderer/lib/reticulum/reticulumConfigAudit.ts`](src/renderer/lib/reticulum/reticulumConfigAudit.ts) | Config audit/repair IPC client |
| [`src/renderer/components/NodeDetailModal.tsx`](src/renderer/components/NodeDetailModal.tsx) | Per-node detail overlay: routing health, MQTT ignore toggle |
| [`src/renderer/components/NodeInfoBody.tsx`](src/renderer/components/NodeInfoBody.tsx) | RF findings section, redundancy path history, congestion block |
| File | Purpose |
| --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| [`src/renderer/stores/diagnosticsStore.ts`](../src/renderer/stores/diagnosticsStore.ts) | Zustand store: anomaly state, persistence, MQTT ignore sets, foreign LoRa records |
| [`src/renderer/lib/diagnostics/RoutingDiagnosticEngine.ts`](../src/renderer/lib/diagnostics/RoutingDiagnosticEngine.ts) | Hop anomaly detection (hop_goblin, bad_route, impossible_hop, route_flapping) |
| [`src/renderer/lib/diagnostics/RFDiagnosticEngine.ts`](../src/renderer/lib/diagnostics/RFDiagnosticEngine.ts) | RF signal analysis (connected node + remote node findings) |
| [`src/renderer/lib/diagnostics/diagnosticRows.ts`](../src/renderer/lib/diagnostics/diagnosticRows.ts) | Row merge/prune utilities, `filterDiagnosticRowsForProtocol`, default max-age values |
| [`src/renderer/lib/foreignLoraDetection.ts`](../src/renderer/lib/foreignLoraDetection.ts) | Foreign LoRa packet classification, Reticulum overhear heuristic, proximity scoring |
| [`src/renderer/components/DiagnosticsPanel.tsx`](../src/renderer/components/DiagnosticsPanel.tsx) | Diagnostics tab UI: health band + counts, anomaly table, foreign LoRa tables, settings |
| [`src/renderer/components/ReticulumDiagnosticsSection.tsx`](../src/renderer/components/ReticulumDiagnosticsSection.tsx) | Reticulum config audit table + repair actions |
| [`src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts`](../src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts) | Reticulum-native diagnostic rows (interfaces, audit merge) |
| [`src/renderer/lib/reticulum/reticulumConfigAudit.ts`](../src/renderer/lib/reticulum/reticulumConfigAudit.ts) | Config audit/repair IPC client |
| [`src/renderer/components/NodeDetailModal.tsx`](../src/renderer/components/NodeDetailModal.tsx) | Per-node detail overlay: routing health, MQTT ignore toggle |
| [`src/renderer/components/NodeInfoBody.tsx`](../src/renderer/components/NodeInfoBody.tsx) | RF findings section, redundancy path history, congestion block |

---

Expand Down
4 changes: 2 additions & 2 deletions docs/reticulum-sidecar-ipc.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,9 @@ Renderer calls `electronAPI.reticulum.*`; main process proxies to this API (sand
| `reticulum:rncpSend` / `rncpFetch` / `setRncpListener` | Picker-gated rncp send/fetch/listener (path must match `reticulum-remote-paths` allowlist) |
| `reticulum:showRncpOpenFileDialog` / `showRncpSaveDirectoryDialog` | Native pickers that seed the rncp send-file / save-dir+fetch-jail allowlists |
| `reticulum:revealInFolder` | Reveal a path in the OS file manager when it matches an rncp picker allowlist |
| `reticulum:onEvent` / `onStatus` | Shared `/ws` events and sidecar status |
| `reticulum:event` / `reticulum:status` | Shared `/ws` events and sidecar status (preload: `onEvent` / `onStatus`) |
| `reticulum:voiceSendAudio` | Dedicated PCM TX ingest (`POST /api/v1/voice/audio`); own ~2000/min budget (not generic `proxyPost`) |
| `reticulum:onVoiceAudio` | Dedicated `/ws/voice` → `reticulum:voiceAudio` PCM frames (`voice.audio`) |
| `reticulum:voiceAudio` | Dedicated `/ws/voice` PCM frames (`voice.audio`; preload: `onVoiceAudio`) |
| `electronAPI.reticulum.voice.*` | Preload surface: `getStatus` / `call` / `answer` / `reject` / `hangup` / `mute` / `sendAudio` |
| `reticulum:gamesStatus` / `gamesApps` / `gamesSessions` / … | Dedicated LRGP games IPC (~600/min); generic proxy rejects `/api/v1/games/*` |
| `electronAPI.reticulum.games.*` | Preload: `getStatus` / `listApps` / `listSessions` / `getSession` / `sendAction` / `resend` / `markRead` / `deleteSession` |
Expand Down
3 changes: 3 additions & 0 deletions scripts/check-insecure-temp-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const WRITE_FNS = [
'createWriteStream',
'copyFileSync',
'copyFile',
// mkdirSync on a predictable tmpdir path is the same class of issue (extract dirs, probes).
'mkdirSync',
'mkdir',
];

const SKIP_DIRS = new Set(['node_modules', 'dist', 'dist-electron', 'coverage', '.git']);
Expand Down
24 changes: 24 additions & 0 deletions scripts/check-insecure-temp-files.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,28 @@ export const mock = () => path.join(os.tmpdir(), 'mesh-client-support-test-userd
`);
expect(result.status).toBe(0);
});

it('fails on mkdirSync to predictable tmpdir path', () => {
const result = runCheckOnSnippet(`
import fs from 'fs';
import os from 'os';
import path from 'path';
const dir = path.join(os.tmpdir(), 'mesh-client-appimage-x64-1');
fs.mkdirSync(dir, { recursive: true });
`);
expect(result.status).toBe(1);
expect(result.stderr).toMatch(/insecure-temporary-file|predictable/);
});

it('fails on async fs.mkdir to predictable tmpdir path', () => {
const result = runCheckOnSnippet(`
import fs from 'fs';
import os from 'os';
import path from 'path';
const dir = path.join(os.tmpdir(), 'mesh-client-appimage-x64-async');
await fs.promises.mkdir(dir, { recursive: true });
`);
expect(result.status).toBe(1);
expect(result.stderr).toMatch(/insecure-temporary-file|predictable/);
});
});
4 changes: 2 additions & 2 deletions scripts/check-log-injection.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Flags console.log/warn/error in src/main that pass raw error-like variables
* (err, e, error, reason) or error-derived content (e.message, String(e), etc.)
* without sanitizeLogMessage() at the call site.
* See CONTRIBUTING.md § Log injection (CodeQL js/log-injection).
* See AGENTS.md §3 (Log injection / sanitizeLogMessage; CodeQL js/log-injection).
*
* To suppress a false positive, add // log-injection-ok with a short reason
* on the same line as the console call.
Expand Down Expand Up @@ -71,7 +71,7 @@ function main() {
console.error('');
}
console.error(
'See CONTRIBUTING.md § Log injection (CodeQL js/log-injection). To suppress, add // log-injection-ok with a reason.',
'See AGENTS.md §3 (Log injection / sanitizeLogMessage). To suppress, add // log-injection-ok with a reason.',
);
process.exit(1);
}
Expand Down
28 changes: 17 additions & 11 deletions scripts/test-linux-appimage-reticulum-sidecar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
existsSync,
fstatSync,
mkdirSync,
mkdtempSync,
openSync,
readSync,
readdirSync,
Expand Down Expand Up @@ -134,9 +135,11 @@ export function findSquashfsOffset(appImagePath) {
return null;
}

/** Prepare a clean extract directory for AppImage --appimage-extract (spawnSync needs existing cwd). */
/**
* Ensure extract cwd exists for AppImage --appimage-extract (spawnSync needs existing cwd).
* Does not delete/recreate `extractDir` — callers pass a unique mkdtemp path that must be preserved.
*/
export function prepareAppImageExtractDir(extractDir) {
rmSync(extractDir, { recursive: true, force: true });
mkdirSync(extractDir, { recursive: true });
}

Expand Down Expand Up @@ -195,15 +198,18 @@ function extractAppImage(appImagePath, extractDir) {

/** @param {'x64' | 'arm64'} arch @param {string} appImagePath */
function assertSidecarInAppImage(arch, appImagePath) {
const extractDir = path.join(tmpdir(), `mesh-client-appimage-${arch}-${process.pid}`);
const payloadRoot = extractAppImage(appImagePath, extractDir);
assertBundledReticulumSidecarInBundle({
label: `${arch} AppImage Reticulum sidecar`,
platform: 'linux',
bundleRoot: payloadRoot,
fail,
});
rmSync(extractDir, { recursive: true, force: true });
const extractDir = mkdtempSync(path.join(tmpdir(), `mesh-client-appimage-${arch}-`));
try {
const payloadRoot = extractAppImage(appImagePath, extractDir);
assertBundledReticulumSidecarInBundle({
label: `${arch} AppImage Reticulum sidecar`,
platform: 'linux',
bundleRoot: payloadRoot,
fail,
});
} finally {
rmSync(extractDir, { recursive: true, force: true });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
console.debug(
`[test-linux-appimage-reticulum-sidecar] OK — sidecar present in ${path.basename(appImagePath)}`,
);
Expand Down
13 changes: 13 additions & 0 deletions scripts/test-linux-appimage-reticulum-sidecar.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ describe('test-linux-appimage-reticulum-sidecar', () => {
}
});

it('prepareAppImageExtractDir preserves an existing unique extract directory', () => {
const extractDir = mkdtempSync(path.join(tmpdir(), 'mesh-appimage-unique-'));
const marker = path.join(extractDir, 'keep-me.txt');
try {
writeFileSync(marker, 'ok');
prepareAppImageExtractDir(extractDir);
expect(existsSync(extractDir)).toBe(true);
expect(existsSync(marker)).toBe(true);
} finally {
rmSync(extractDir, { recursive: true, force: true });
}
});

it('readElfMachineFromHeader reads e_machine from ELF header bytes', () => {
expect(readElfMachineFromHeader(makeElfHeader(EM_X86_64))).toBe(EM_X86_64);
expect(readElfMachineFromHeader(makeElfHeader(EM_AARCH64))).toBe(EM_AARCH64);
Expand Down
Loading