From 57bf80b824d5c474c76338b429743b4588b1440c Mon Sep 17 00:00:00 2001 From: zrack Date: Sat, 25 Jul 2026 14:58:19 -0700 Subject: [PATCH] feat: add field capture readiness --- .github/workflows/release.yml | 29 +- ACCESSIBILITY.md | 8 +- CHANGELOG.md | 10 +- CONTRIBUTING.md | 5 +- DECODER_PACKS.md | 7 +- README.md | 39 +- SECURITY.md | 5 +- USER_GUIDE.md | 89 +- USE_CASES.md | 26 +- design-qa.md | 44 +- docs/design/capture-preflight-ready.png | Bin 0 -> 128097 bytes docs/design/capture-profile-mobile.png | Bin 0 -> 38878 bytes docs/design/capture-profile-setup.png | Bin 0 -> 125277 bytes docs/releases/v0.2.0.md | 45 + package-lock.json | 4 +- package.json | 2 +- scripts/cli-smoke.mjs | 4 +- scripts/narrowslink.ts | 2 +- src/capture/CaptureDialog.test.ts | 9 + src/capture/CaptureDialog.tsx | 912 +++++++++++++++++-- src/capture/capture-preflight.test.ts | 103 +++ src/capture/capture-preflight.ts | 191 ++++ src/capture/capture-profile.test.ts | 133 +++ src/capture/capture-profile.ts | 312 +++++++ src/capture/index.ts | 2 + src/capture/web-serial.test.ts | 43 + src/capture/web-serial.ts | 27 +- src/styles.css | 140 +++ tests/e2e/capture-to-evidence.spec.ts | 67 +- tests/e2e/comparison-workspace.spec.ts | 6 +- tests/e2e/serial-capture-to-evidence.spec.ts | 12 +- tests/release/capture-to-evidence.spec.ts | 9 +- 32 files changed, 2068 insertions(+), 217 deletions(-) create mode 100644 docs/design/capture-preflight-ready.png create mode 100644 docs/design/capture-profile-mobile.png create mode 100644 docs/design/capture-profile-setup.png create mode 100644 docs/releases/v0.2.0.md create mode 100644 src/capture/capture-preflight.test.ts create mode 100644 src/capture/capture-preflight.ts create mode 100644 src/capture/capture-profile.test.ts create mode 100644 src/capture/capture-profile.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d51ef69..e3d327d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release on: push: tags: - - "v0.1.0" + - "v*.*.*" permissions: contents: read @@ -35,6 +35,9 @@ jobs: test "$(git cat-file -t "refs/tags/${RELEASE_TAG}")" = "tag" test "$(git rev-parse "refs/tags/${RELEASE_TAG}^{}")" = "${GITHUB_SHA}" + - name: Resolve release version + run: echo "RELEASE_VERSION=${GITHUB_REF_NAME#v}" >> "${GITHUB_ENV}" + - name: Set up Node.js uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: @@ -58,7 +61,7 @@ jobs: - name: Test unpacked release bytes env: - NARROWSLINK_RELEASE_ARCHIVE: build/release/narrowslink-0.1.0.tgz + NARROWSLINK_RELEASE_ARCHIVE: build/release/narrowslink-${{ env.RELEASE_VERSION }}.tgz run: npm run test:release - name: Verify published checksum set @@ -70,9 +73,9 @@ jobs: with: name: narrowslink-release-${{ github.ref_name }} path: | - build/release/narrowslink-0.1.0.tgz - build/release/narrowslink-0.1.0.release.json - build/release/narrowslink-0.1.0.cdx.json + build/release/narrowslink-*.tgz + build/release/narrowslink-*.release.json + build/release/narrowslink-*.cdx.json build/release/SHA256SUMS if-no-files-found: error retention-days: 14 @@ -106,14 +109,16 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - test "${GITHUB_REF_NAME}" = "v0.1.0" - test -f "docs/releases/v0.1.0.md" + RELEASE_VERSION="${GITHUB_REF_NAME#v}" + test "v${RELEASE_VERSION}" = "${GITHUB_REF_NAME}" + test -f "docs/releases/${GITHUB_REF_NAME}.md" + test -f "build/release/narrowslink-${RELEASE_VERSION}.tgz" gh release create "${GITHUB_REF_NAME}" \ - build/release/narrowslink-0.1.0.tgz \ - build/release/narrowslink-0.1.0.release.json \ - build/release/narrowslink-0.1.0.cdx.json \ + "build/release/narrowslink-${RELEASE_VERSION}.tgz" \ + "build/release/narrowslink-${RELEASE_VERSION}.release.json" \ + "build/release/narrowslink-${RELEASE_VERSION}.cdx.json" \ build/release/SHA256SUMS \ --repo "${GITHUB_REPOSITORY}" \ --verify-tag \ - --title "NarrowsLink v0.1.0" \ - --notes-file "docs/releases/v0.1.0.md" + --title "NarrowsLink ${GITHUB_REF_NAME}" \ + --notes-file "docs/releases/${GITHUB_REF_NAME}.md" diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md index 8e58237..9bf9e62 100644 --- a/ACCESSIBILITY.md +++ b/ACCESSIBILITY.md @@ -11,12 +11,12 @@ The Playwright suite runs against Chromium, Firefox, and WebKit in `npm run chec | Bundled and local replay | Pass | Pass | Pass | Invalid import recovery, playback, seeking, rate changes, and active-replay preservation | | Local session library | Pass | Pass | Pass | Save, exact-content deduplication, reload, validated reopen, workspace restoration, guarded removal, and unavailable/quota/corruption/cleanup failures | | Maximum-record processing | Pass | Pass | Pass | A 200,000-record replay is imported, persisted, cancelled during reopen without state loss, reopened, compared over an exact bounded range, cancelled during bundle construction without a download, rebuilt, and verified while a one-second heartbeat monitors interaction responsiveness | -| UDP capture-to-evidence loop | Pass | Pass | Pass | Real loopback NSL-01 and NMEA capture, reconciled v2 receipt, `.nlsession` download and reimport, replay, exact incident export, independent archive verification, in-application receipt, reload, and failure recovery | -| Simulated Web Serial capture-to-evidence loop | Pass | Pass | Pass | Injected browser API, fragmented reads, complete and partial NSL-01 assembly, reconciled v2 receipt, durable reopen, replay, exact authored range, `.nlb` generation, and independent archive verification | +| UDP capture-to-evidence loop | Pass | Pass | Pass | Real loopback NSL-01 and NMEA preflight, decoder-fit and mismatch states, explicit new-capture boundary, reconciled v2 receipt, `.nlsession` download and reimport, replay, exact incident export, independent archive verification, in-application receipt, reload, and failure recovery | +| Simulated Web Serial capture-to-evidence loop | Pass | Pass | Pass | Injected browser API, preflight on one selected device, future-read handler transfer without reopen, fragmented reads, complete and partial NSL-01 assembly, reconciled v2 receipt, durable reopen, replay, exact authored range, `.nlb` generation, and independent archive verification | | Received evidence workspace | Pass | Pass | Pass | Exact bounded incident, separate verification claims, explicit unavailable groups, decoded and raw evidence, provenance, source annotations, separate receiver finding, rejected-archive recovery, reload, and unpacked-release replacement | | Comparative replay workspace | Pass | Pass | Pass | Explicit baseline and candidate setup, validated session and verified-bundle input, aligned range control, eligibility matrix, metric selection, bounded source trace, authored conclusion, finding export, and clean state for a new comparison | | axe rules tagged WCAG A/AA | Pass | Pass | Pass | Automated scans of the replay, receiver, and comparison workspaces plus live-capture, range, marker, bundle, comparison-setup, and long-running processing dialogs | -| Keyboard and focus handoff | Pass | Pass | Pass | Dialog entry/return, incident tabs, range deletion, incident clear/select, capture start-to-recording, captured-session replacement, comparison setup, and authored finding controls | +| Keyboard and focus handoff | Pass | Pass | Pass | Dialog entry/return, incident tabs, range deletion, incident clear/select, capture ready-to-preflight-to-recording transitions, captured-session replacement, comparison setup, and authored finding controls | | Reflow and horizontal evidence access | Pass | Pass | Pass | Replay, receiver, and comparison workspaces at `960 × 900`, `640 × 900`, and `390 × 844`; no page-level horizontal overflow, wrapped command actions, and explicit keyboard panning for table or timeline scrollers | | Forced-color and non-color cues | Pass | Pass | Pass | Selected incidents expose pressed state; diagnostics retain visible severity words or `C`/`W`/`I` tokens in addition to color | @@ -27,7 +27,7 @@ Playwright's WebKit project provides browser-engine coverage. It does not by its ## Keyboard contract - Opening a dialog moves focus to its first meaningful field or action, traps `Tab` within the modal, and returns focus to the opener when the dialog closes. -- Live capture moves focus to a valid control or status target whenever setup, recording, stopping, saving, recovery, or discard replaces the previously focused action. +- Live capture moves focus to a valid control or status target whenever ready, preflight, recording, stopping, saving, recovery, or discard replaces the previously focused action. - Incident tabs support arrow-key navigation with one selected and tabbable tab. - Clearing an incident focuses the empty selection state; selecting the first incident or deleting an authored range focuses the replacement selector or empty state. - Delete and discard confirmations focus the safe action first and return focus to the initiating control when cancelled. diff --git a/CHANGELOG.md b/CHANGELOG.md index a3b7134..56440c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,13 @@ All notable changes to NarrowsLink are recorded here. This is the canonical proj ## [Unreleased] +## [0.2.0] - 2026-07-25 + ### Added +- Added reusable local capture profiles that preserve validated UDP or serial settings and the exact decoder pack while explicitly excluding bridge credentials, browser device permission, session names, and telemetry payloads. +- Added bounded UDP and Web Serial preflight before recording, with live source state, traffic and byte rates, last-input age, valid and malformed frame counts, checksum failures, message families, endpoint observations, no-traffic guidance, and decoder-mismatch guidance. +- Added an explicit preflight-to-evidence boundary: UDP probes are stopped and discarded before a new owned capture ID begins, while serial retains the selected port but resets framing and routes only future reads into the immutable session. - Added bounded, content-addressed decoder packs with an allowlisted parser runtime, canonical pack and schema identities, production-path conformance fixtures, local operator loading, and packaged `decoder seal` and `decoder validate` commands; new captures persist the exact pack and runtime while legacy NSL-01 sessions remain compatible and unchanged. - Added the NMEA 0183 reference pack for checksummed GGA, RMC, and HDT sentences over one-sentence-per-datagram UDP or line-delimited serial input, including partial-tail retention, checksum diagnostics, real loopback UDP capture, local-library reopen, evidence export, and production receiver replay verification. - Added an in-application receiver workspace for untrusted version 3 `.nlb` bundles: worker-isolated production verification now precedes inspection, the exact bounded incident is reconstructed only from included evidence, excluded context remains explicit, and internal consistency, evidence completeness, and unsigned authenticity remain separate claims. @@ -18,6 +23,8 @@ All notable changes to NarrowsLink are recorded here. This is the canonical proj ### Changed +- Promoted decoder packs, NMEA 0183, the in-application receiver, comparative replay, worker-backed large-session processing, and field-capture preflight into the self-contained v0.2 operator distribution. +- Generalized the annotated-tag release workflow so the package version, release notes, asset names, and GitHub Release remain bound to one `v` identity. - Defined NarrowsLink's product goal and success criteria around reproducible constrained-telemetry incidents and independently verified handoffs; retained the strategic product directions and moonshot opportunities while keeping delivered capabilities in the changelog and current-state docs. - Expanded UC-001 with supported radio transport capture topologies, including USB serial radio, UDP base-station output, forwarded UDP copies, and network multicast observer setup and test paths. - Raised the imported and saved replay envelope from 32 MiB and 100,000 records to 64 MiB and 200,000 records while retaining the separate live-capture ceiling of 100,000 records, 32 MiB of retained payload bytes, 24 hours, and a canonical file within the replay limit. @@ -55,5 +62,6 @@ All notable changes to NarrowsLink are recorded here. This is the canonical proj - Restricted the UDP bridge control plane to loopback access with server-enforced capture ownership and an internal short-lived bearer credential; the browser uses a same-origin application relay, so the credential is not exposed in runtime metadata, URLs, cookies, readiness output, or logs. -[Unreleased]: https://github.com/zrack/narrowslink/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/zrack/narrowslink/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/zrack/narrowslink/releases/tag/v0.2.0 [0.1.0]: https://github.com/zrack/narrowslink/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87c35fd..0200376 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -142,6 +142,8 @@ Changing a description, schema, fixture, or expected result changes pack identit - Re-hash, parse, and validate every saved-session reopen. Surface corruption and storage failures without replacing the active validated replay or claiming an uncommitted save succeeded. - Treat saved-session removal as a two-store operation: delete the IndexedDB replay, clear its per-session local-storage workspace when identifiable, keep any active replay in memory, and warn if residual operator context could not be cleared. - Never let a control client stop or adopt a UDP capture it did not start; reconcile bridge sequence, datagram, and byte totals before claiming a capture is complete. +- Keep saved capture profiles bounded and local, bind them to an exact conformant decoder pack, and exclude credentials, browser device permission, session naming, and telemetry payloads. +- Keep preflight bounded and outside session evidence. Require confirmed UDP probe shutdown before opening a new recording identity; for serial, keep the selected port but reset framing and route only future reads to the recorder. - New live captures must finalize as session v2 with immutable transport events and a terminal receipt. Preserve strict v1 import behavior and never infer verified integrity for legacy evidence. - Never substitute browser or recorder counts for an unavailable transport-reported count. Preserve null observations, use the truthful incomplete assessment basis, and require explicit adapter evidence before verification. - Preserve the bridge journal, per-datagram UDP remote endpoint, and Web Serial device and negotiated-setting evidence when changing capture code. Keep unavailable operating-system counters explicitly null with their observation source; do not convert missing evidence into a clean zero. @@ -172,6 +174,7 @@ Changing a description, schema, fixture, or expected result changes pack identit | Evidence receiver verification and CLI | `verifier/`, `scripts/narrowslink.ts`, `vite.cli.config.ts` | | Session serialization and import behavior | `src/data/session-file.ts`, `src/data/load-session.ts` | | Capture lifecycle and session finalization | `src/capture/CaptureDialog.tsx`, `src/capture/recorder.ts` | +| Capture profiles and bounded preflight | `src/capture/capture-profile.ts`, `src/capture/capture-preflight.ts` | | Serial capture and runtime-selected assembly | `src/capture/web-serial.ts`, `src/capture/serial-assembler.ts`, `src/capture/nsl01-serial-assembler.ts`, `src/capture/nmea0183-serial-assembler.ts` | | UDP browser protocol and local bridge | `src/capture/udp-bridge.ts`, `scripts/capture-bridge.mjs` | | Durable session-document library | `src/storage/session-library.ts` | @@ -206,7 +209,7 @@ Screenshots are evidence, not the review itself. Keep `design-qa.md` focused on - For large-session changes, regenerate the 200,000-record corpus and exercise import, progress, cancellation, exact-byte persistence, reload, reopen, selected-range comparison, bundle cancellation, completed export, production verification, heartbeat gaps, and Chromium heap growth in both the source and unpacked-release matrices. - For UI changes, exercise the bundled replay, file-import error state, saved-session save/list/reopen/remove and failure states, playback, seeking, incident switching, marker creation, note persistence, and bundle flow at desktop and narrow widths. - Keep critical dialogs and workspace states clean under axe rules tagged WCAG A/AA. Test keyboard focus when an action, dialog phase, incident selection, or responsive layout replaces the focused element; retain visible non-color state and severity cues. -- For capture changes, exercise both the real loopback UDP path and the browser-injected Web Serial application path from start through stop, re-import, durable reopen, replay, annotation, bundle export, and production receiver verification. Include active-capture ownership, sequence gaps, zero-length datagrams, and corrupt serial-length resynchronization in automated coverage. Treat the injected serial gate as application-path evidence, not physical hardware certification. +- For capture changes, exercise both the real loopback UDP path and the browser-injected Web Serial application path from profile or direct setup through preflight, the explicit recording boundary, stop, re-import, durable reopen, replay, annotation, bundle export, and production receiver verification. Include decoder mismatch, no probe leakage, active-capture ownership, unconfirmed probe shutdown, sequence gaps, zero-length datagrams, and corrupt serial-length resynchronization in automated coverage. Treat the injected serial gate as application-path evidence, not physical hardware certification. - For integrity changes, add a failure round trip through capture finalization, JSON serialization, replay parsing, operator-authored half-open range projection, bundle generation, and production receiver verification. Cover both a UDP and serial path when the contract affects both. ## Contribution workflow diff --git a/DECODER_PACKS.md b/DECODER_PACKS.md index 4f2374e..829ece5 100644 --- a/DECODER_PACKS.md +++ b/DECODER_PACKS.md @@ -12,9 +12,10 @@ The current application ships two packs: 1. Open **Live capture**. 2. Choose a bundled pack under **Decoder pack**, or select **Load pack** and choose a local `.nldecoder` or `.json` file. 3. Wait for the loaded-pack notice. NarrowsLink validates the file shape, canonical SHA-256 identity, supported runtime, schema compatibility, and every bundled fixture before making the pack active. -4. Configure UDP or serial and start capture. The selected pack is locked for that capture. -5. Stop and save. The version 2 `.nlsession` embeds the exact pack and records its pack, schema, runtime, and revision identities. -6. Export an `.nlb` with **Decoder schema** included. The production receiver revalidates the embedded pack and reproduces decoded rows from the selected raw records. +4. Configure UDP or serial, run preflight with known traffic, and confirm that the selected pack produces the expected valid frames and message families. The pack is locked once preflight begins. +5. Select **Start recording**. UDP replaces the discarded probe with a new capture identity; serial resets framing on the already selected port so only subsequent reads enter evidence. +6. Stop and save. The version 2 `.nlsession` embeds the exact pack and records its pack, schema, runtime, and revision identities. +7. Export an `.nlb` with **Decoder schema** included. The production receiver revalidates the embedded pack and reproduces decoded rows from the selected raw records. If pack validation or a fixture fails, capture does not start with that pack. Existing raw session records are never rewritten to fit a replacement decoder. diff --git a/README.md b/README.md index 9ffee56..5e389e4 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ NarrowsLink records live UDP or serial telemetry through an identified, content- ![NarrowsLink mission-timeline session review workspace](docs/assets/narrowslink-dashboard.png) -The application is local-first. The v0.1 distribution starts the production workspace and authenticated UDP bridge together on loopback; the browser uses a same-origin application relay, while the bridge credential remains internal to the managed process and never requires operator copying. Its UDP socket binds the operator-selected interface. Serial ingest uses the browser's Web Serial connection. Capture, saved sessions, worker-isolated replay processing, annotations, comparison, evidence generation, and receiver verification stay on the operator's or receiving engineer's machine. NarrowsLink has no telemetry upload, cloud account, or hosted dependency. +The application is local-first. The v0.2 distribution starts the production workspace and authenticated UDP bridge together on loopback; the browser uses a same-origin application relay, while the bridge credential remains internal to the managed process and never requires operator copying. Its UDP socket binds the operator-selected interface. Serial ingest uses the browser's Web Serial connection. Capture, saved sessions, worker-isolated replay processing, annotations, comparison, evidence generation, and receiver verification stay on the operator's or receiving engineer's machine. NarrowsLink has no telemetry upload, cloud account, or hosted dependency. ## Start here - Follow the [user guide](USER_GUIDE.md) for installation, live capture, replay, incident authoring, evidence handoff, upgrades, removal, and troubleshooting. -- Download the current package and release evidence from [NarrowsLink v0.1.0](https://github.com/zrack/narrowslink/releases/tag/v0.1.0). +- Download the current package and release evidence from [NarrowsLink v0.2.0](https://github.com/zrack/narrowslink/releases/tag/v0.2.0). - Review the [use-case log](USE_CASES.md) for supported operator outcomes and current constraints. - Use the [decoder-pack guide](DECODER_PACKS.md) to load, author, seal, validate, and hand off a protocol definition. - Use [SUPPORT.md](SUPPORT.md) to prepare a reproducible support request without disclosing sensitive telemetry. @@ -19,22 +19,20 @@ The application is local-first. The v0.1 distribution starts the production work ## Quick start -NarrowsLink v0.1.0 is a self-contained package with zero runtime npm dependencies. It contains the production UI, managed UDP bridge, deterministic Harbor relay fixture, and receiver verifier. It requires Node.js 20.19 or newer, but it does not require a repository checkout, Vite, or project dependencies. +NarrowsLink v0.2.0 is a self-contained package with zero runtime npm dependencies. It contains the production UI, managed UDP bridge, deterministic Harbor relay fixture, decoder-pack tools, application and CLI evidence receivers, and comparison workflow. It requires Node.js 20.19 or newer, but it does not require a repository checkout, Vite, or project dependencies. -The decoder-pack, NMEA, in-application receiver, comparative replay, and large-session processing capabilities documented below are currently under `[Unreleased]` in this repository and are not present in the tagged v0.1.0 package. +Download these four assets from the [v0.2.0 GitHub Release](https://github.com/zrack/narrowslink/releases/tag/v0.2.0): -Download these four assets from the [v0.1.0 GitHub Release](https://github.com/zrack/narrowslink/releases/tag/v0.1.0): - -- `narrowslink-0.1.0.tgz` -- `narrowslink-0.1.0.release.json` -- `narrowslink-0.1.0.cdx.json` +- `narrowslink-0.2.0.tgz` +- `narrowslink-0.2.0.release.json` +- `narrowslink-0.2.0.cdx.json` - `SHA256SUMS` On macOS, verify the assets, install the package without lifecycle scripts, confirm its identity, and start the application: ```bash shasum -a 256 -c SHA256SUMS -npm install --global ./narrowslink-0.1.0.tgz --ignore-scripts +npm install --global ./narrowslink-0.2.0.tgz --ignore-scripts narrowslink version --json narrowslink serve ``` @@ -50,6 +48,9 @@ Continue with the [guided first run](USER_GUIDE.md#first-run-with-the-bundled-re ## Current capabilities - Capture unicast or multicast UDP datagrams through the managed authenticated local bridge, or assemble serial records directly through Web Serial using the selected decoder pack. Stopping a source downloads a versioned `.nlsession`, opens the validated finalized capture for replay, and attempts to retain it in the local session library. +- Save up to 16 local capture profiles containing the exact validated decoder pack and UDP or serial settings. Profiles deliberately exclude bridge credentials, browser device permission, session names, and telemetry payloads. +- Preflight a live UDP or serial source before evidence recording. The bounded probe reports traffic and byte rates, last-input age, valid and malformed frames, checksum failures, observed message families, endpoints, and decoder fit without retaining sampled payloads as session evidence. +- Start evidence collection through an explicit boundary. UDP stops and discards the probe before opening a new capture identity; serial retains the selected port while resetting framing and routing only future reads into the immutable session. - Choose the bundled NSL-01 or NMEA 0183 reference pack, or load a local bounded declarative pack. NarrowsLink checks canonical pack identity, runtime and schema compatibility, and bundled production-path fixtures before capture; it never executes pack-supplied JavaScript. - Load the bundled demonstration, reopen a saved session, or choose a local version 1 or 2 session. Imported and saved sessions are read, validated, decoded, aggregated, canonicalized, and transferred through a worker-backed processing contract with visible phase progress and cancellation. A failed or cancelled operation leaves the current replay unchanged and never persists partial content. Legacy v1 evidence remains unchanged and carries an explicit unknown capture-integrity assessment. - Keep multiple validated sessions in an IndexedDB-backed local library. The Sessions rail lists real title, time, duration, and integrity metadata; exact duplicate content remains one entry, and every reopen rechecks the stored SHA-256 identity, canonical session bytes, validation, and decoding before replacing the active replay. New saves use exact canonical bytes in the version 3 library record while version 1 text and version 2 Blob records remain readable. Removing an entry also clears its separately stored markers, note, and authored ranges when browser storage permits; an active replay stays open until it is replaced. @@ -79,7 +80,7 @@ See the canonical [use-case log](USE_CASES.md) for actors, supported workflows, NarrowsLink opens the bundled **Harbor relay downlink** replay automatically. An operator can replace it with a validated local file, reopen a saved session, or record live UDP or serial traffic. The normal capture-to-evidence path is: -1. Select or load the decoder pack, then open or capture a session and select a replay preset or operator-authored incident range. +1. Apply or create a capture profile, preflight known traffic against the selected decoder, then deliberately start evidence recording. Alternatively, open an existing session and select a replay preset or operator-authored incident range. 2. Correlate link health, packet cadence, decoder state, diagnostics, decoded signals, and transport provenance on the shared replay clock. 3. Add local markers and a session note without changing the captured records. 4. Choose optional evidence groups; the transport event log, provenance, bridge journal, and capture-integrity receipt remain mandatory. @@ -98,13 +99,13 @@ Uninstalling the package does not delete the local session library, operator wor ## Live capture -![NarrowsLink live UDP capture setup](docs/design/live-capture-setup.jpg) +![NarrowsLink confirming UDP traffic and decoder fit before recording](docs/design/capture-preflight-ready.png) The installed release offers **UDP bridge** and **Serial port** from the **Live capture** dialog. -For UDP, the managed process keeps its bridge credential internal and exposes bind, port, and optional multicast settings in the dialog. Command-line UDP flags populate dialog defaults; no socket starts until the operator selects **Start UDP capture**. The chosen bind controls which local interface receives telemetry, so `0.0.0.0` should be used only when listening on every local IPv4 interface is intentional. +For UDP, the managed process keeps its bridge credential internal and exposes bind, port, and optional multicast settings in the dialog. Command-line UDP flags populate dialog defaults; no socket starts until the operator selects **Run UDP preflight**. The preflight uses a temporary capture identity and retains only bounded aggregate observations. **Start recording** stops and discards that probe before opening a new capture identity, so probe traffic cannot silently become evidence. The chosen bind controls which local interface receives telemetry, so `0.0.0.0` should be used only when listening on every local IPv4 interface is intentional. -For serial, the operator selects port settings and then chooses the device through the browser's native Web Serial prompt. Web Serial requires a supporting Chromium browser and a secure loopback context. Automated coverage exercises the application path with an injected standards-based serial API; physical adapters, drivers, native chooser behavior, and operating-system disconnect handling remain a manual boundary. +For serial, the operator selects port settings and then chooses the device through the browser's native Web Serial prompt with **Select port & preflight**. **Start recording** keeps that open port, resets the framing state, and sends only subsequent reads to the recorder. Web Serial requires a supporting Chromium browser and a secure loopback context. Automated coverage exercises the application path with an injected standards-based serial API; physical adapters, drivers, native chooser behavior, and operating-system disconnect handling remain a manual boundary. Stopping either source with **Stop, save & replay** downloads a version 2 `.nlsession`, opens the validated finalized capture, and attempts to retain it in the local session library. Follow the [UDP procedure](USER_GUIDE.md#record-live-udp) or [serial procedure](USER_GUIDE.md#record-live-serial-telemetry) before connecting a field source. @@ -214,13 +215,13 @@ Range-scoped events, records, decoded packets, diagnostics, markers, and notes a ### Receive and verify a bundle -In the current repository build, choose **Open evidence** in the Sessions rail or top bar and select the `.nlb`. NarrowsLink reads the file as untrusted input, verifies it in a worker with the production verifier, and does not replace the current workspace unless every required archive and semantic check succeeds. +In NarrowsLink v0.2.0, choose **Open evidence** in the Sessions rail or top bar and select the `.nlb`. NarrowsLink reads the file as untrusted input, verifies it in a worker with the production verifier, and does not replace the current workspace unless every required archive and semantic check succeeds. The receiver workspace preserves the source session identity and exact half-open selection while projecting only included raw records, decoded packets, diagnostics, source annotations, and transport evidence. Excluded artifact groups and unavailable whole-session context remain explicit. Internal consistency, evidence completeness, and source authenticity are shown as separate claims. The **Notes** tab stores a receiver-owned finding separately under the whole-bundle SHA-256; it does not alter the received bytes or become source evidence. ![NarrowsLink received incident evidence workspace](docs/design/receiver-workspace.png) -The CLI remains available when a terminal or stable JSON report is required. After installing the v0.1.0 package, a receiving engineer can verify a version 3 bundle locally without a repository checkout, application server, browser workspace, or network access: +The CLI remains available when a terminal or stable JSON report is required. After installing the v0.2.0 package, a receiving engineer can verify a version 3 bundle locally without a repository checkout, application server, browser workspace, or network access: ```bash narrowslink verify path/to/incident.nlb @@ -253,6 +254,8 @@ The semantic validator rejects altered hashes and internally contradictory range | --- | --- | | `src/App.tsx` | Application state and mission-timeline workspace UI | | `src/capture/CaptureDialog.tsx` | Live-source configuration, capture lifecycle, integrity status, save, and replay handoff | +| `src/capture/capture-profile.ts` | Bounded local profile validation, persistence, and exact decoder-pack recall | +| `src/capture/capture-preflight.ts` | Bounded payload-free source observations and decoder-fit assessment before recording | | `src/capture/recorder.ts` | Bounded immutable source-record collection and versioned session finalization | | `src/capture/web-serial.ts` | Permission-aware Web Serial lifecycle and byte-stream reads | | `src/capture/nsl01-serial-assembler.ts` | NSL-01 framing, noise retention, and bounded resynchronization | @@ -301,7 +304,9 @@ Local does not automatically mean safe to share. A saved replay or evidence bund ## Current limits - Live capture supports UDP and Web Serial; TCP and other transports are not implemented. -- The v0.1 package requires a compatible local Node.js runtime and browser. It bundles all NarrowsLink application code and runtime dependencies, but it is not a native installer or embedded-browser distribution. +- The v0.2 package requires a compatible local Node.js runtime and browser. It bundles all NarrowsLink application code and runtime dependencies, but it is not a native installer or embedded-browser distribution. +- Capture profiles are local convenience state, not evidence or secrets storage. They are limited to 16 profiles and 2 MiB of canonical content, and cannot restore Web Serial permission. +- Preflight analysis is limited to 256 records, 512 KiB of sampled input, and 16 observed endpoints. It stores aggregate observations only; traffic seen before **Start recording** is intentionally absent from the resulting session. - Decoder packs are limited to the built-in bounded runtime allowlist. The current declarative external runtime supports checksummed NMEA 0183 sentence schemas; arbitrary JavaScript, automatic protocol detection, competing decoders, and fundamentally new wire protocols without a reviewed runtime are not supported. - NMEA UDP capture expects one complete sentence per datagram. NMEA serial capture uses line-feed boundaries and retains overlong or unterminated input as partial evidence. - Imported and saved replay documents are limited to 64 MiB of canonical UTF-8 JSON, 200,000 records, and 24 hours. The release gate exercises 200,000 records at 52,378,445 bytes in every supported engine; it does not claim that every browser and machine has identical performance near the hard byte limit. diff --git a/SECURITY.md b/SECURITY.md index 63ef54e..6b5529d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,8 +7,9 @@ NarrowsLink is a pre-1.0 project under active development. Security fixes are ma | Version | Supported | | --- | --- | | Current `main` | Yes | -| Latest `0.1.x` patch | Yes | -| Superseded `0.1.x` patches | No guaranteed backports | +| Latest `0.2.x` patch | Yes | +| Superseded `0.2.x` patches | No guaranteed backports | +| `0.1.x` | No guaranteed backports | | `< 0.1.0`, older commits, and forks | No | ## Report a vulnerability privately diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 7f4a125..2245463 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1,8 +1,6 @@ # NarrowsLink user guide -This guide covers NarrowsLink operation from installation through local capture, replay, incident authoring, evidence export, and receiver verification. Source contributors should use [CONTRIBUTING.md](CONTRIBUTING.md). - -The installation section remains specific to the tagged v0.1.0 package. Decoder-pack, NMEA, in-application receiver, comparative replay, and large-session processing procedures are available in the current repository under `[Unreleased]` and will require a later packaged release. +This guide covers NarrowsLink v0.2.0 operation from installation through local capture, replay, incident authoring, evidence export, receiver verification, and comparative replay. Source contributors should use [CONTRIBUTING.md](CONTRIBUTING.md). NarrowsLink keeps telemetry, saved sessions, operator annotations, and evidence generation on the local machine. It does not provide accounts, cloud storage, hosted ingestion, or telemetry upload. @@ -14,17 +12,17 @@ You need: - A local browser. - A supported Chromium browser for physical Web Serial capture. - Enough browser memory and storage for the sessions you plan to process and retain. -- The four files from the [NarrowsLink v0.1.0 release](https://github.com/zrack/narrowslink/releases/tag/v0.1.0). +- The four files from the [NarrowsLink v0.2.0 release](https://github.com/zrack/narrowslink/releases/tag/v0.2.0). -The release package contains the production application, authenticated UDP bridge, bundled Harbor relay replay, and evidence receiver CLI. A source checkout, Vite, and project dependencies are not required. +The release package contains the production application, authenticated UDP bridge, bundled Harbor relay replay, decoder-pack tools, application and CLI evidence receivers, and comparison workflow. A source checkout, Vite, and project dependencies are not required. -## Install and verify v0.1.0 +## Install and verify v0.2.0 Download these four release assets into one directory: -- `narrowslink-0.1.0.tgz` -- `narrowslink-0.1.0.release.json` -- `narrowslink-0.1.0.cdx.json` +- `narrowslink-0.2.0.tgz` +- `narrowslink-0.2.0.release.json` +- `narrowslink-0.2.0.cdx.json` - `SHA256SUMS` On macOS, verify the published checksum set: @@ -44,7 +42,7 @@ All three listed assets must report `OK`. The checksum file is delivered through Install the package without running lifecycle scripts: ```bash -npm install --global ./narrowslink-0.1.0.tgz --ignore-scripts +npm install --global ./narrowslink-0.2.0.tgz --ignore-scripts ``` Confirm the installed identity: @@ -53,13 +51,7 @@ Confirm the installed identity: narrowslink version --json ``` -For v0.1.0, the output must identify version `0.1.0` and commit: - -```text -a9d6c6b737086d4240accc347a0a5d62f76d9531 -``` - -Compare that output with `narrowslink-0.1.0.release.json`. If the version or commit differs, stop and resolve the package mismatch before collecting evidence. +The output must identify version `0.2.0`. Compare its full commit and version with `narrowslink-0.2.0.release.json`. If either differs, stop and resolve the package mismatch before collecting evidence. ## Start and stop NarrowsLink @@ -85,7 +77,7 @@ Press `Ctrl+C` in the serving terminal to stop the application server and bridge Keep the default application port when you want access to an existing local session library. Browser storage belongs to the exact origin `http://127.0.0.1:47890`; another application port or browser profile selects different storage. -Use `narrowslink serve --help` to inspect bind and launch options. UDP-related command-line flags populate the capture dialog defaults. They do not start a UDP socket until you select **Start UDP capture**. +Use `narrowslink serve --help` to inspect bind and launch options. UDP-related command-line flags populate the capture dialog defaults. They do not start a UDP socket until you select **Run UDP preflight**. ## First run with the bundled replay @@ -142,15 +134,27 @@ Every new capture uses one decoder pack. The default is **NSL-01 v1.3.7**. 4. Wait for the loaded-pack notice. Do not begin a test if identity, compatibility, or fixture validation fails. 5. Confirm the displayed runtime revision and first 12 characters of the pack SHA-256 against the expected pack identity. -Pack selection is locked once capture setup begins. The resulting `.nlsession` embeds the exact pack, schema, runtime, and revision identities. NarrowsLink accepts only bounded declarative packs for its supported runtime allowlist and does not run pack-supplied JavaScript. +Pack selection is locked once preflight begins. The resulting `.nlsession` embeds the exact pack, schema, runtime, and revision identities. NarrowsLink accepts only bounded declarative packs for its supported runtime allowlist and does not run pack-supplied JavaScript. For pack authoring, offline validation, NMEA record boundaries, and the trust model, use [DECODER_PACKS.md](DECODER_PACKS.md). +## Save and reuse a capture profile + +A capture profile is local setup state, not capture evidence. It stores the exact validated decoder pack plus either UDP bind, port, and multicast settings or serial line settings. It never stores the bridge credential, browser device permission, session title, or sampled telemetry. + +1. Configure the decoder and transport in **Live capture**. +2. Select **Save setup**, enter a profile name, and confirm the save. +3. On a later run, choose the profile under **Capture profile**. NarrowsLink applies its exact decoder pack and transport settings. +4. If you deliberately change that setup, the profile is marked **modified**. Select **Update setup** to replace the stored settings or leave the profile unchanged. +5. Use the trash button to remove the selected profile. + +Profiles are stored in browser local storage at the current NarrowsLink origin. They are limited to 16 profiles and 2 MiB of canonical content. A serial profile can restore settings but cannot bypass the browser's native device-selection and permission prompt. + ## Record live UDP The installed release manages the authenticated bridge. The operator never copies a bearer token. -![NarrowsLink live UDP capture setup](docs/design/live-capture-setup.jpg) +![NarrowsLink confirming UDP traffic and decoder fit before recording](docs/design/capture-preflight-ready.png) 1. Start NarrowsLink with `narrowslink serve`. 2. Select **Live capture** or **Capture**. @@ -161,13 +165,18 @@ The installed release manages the authenticated bridge. The operator never copie 7. Select or load the decoder pack that matches the incoming datagrams. 8. Set **UDP bind host** and **UDP port**. 9. For multicast, set **Multicast group** and, when needed, **Multicast interface**. The bind address and group must use the same IP family. -10. Select **Start UDP capture**. -11. Confirm the status is **Recording** and send telemetry to the exact address shown under **Source**. -12. Watch **Datagrams received**, **Input bytes**, **Records retained**, **Bytes retained**, and **Bridge state**. -13. Select **Stop, save & replay**. +10. Select **Run UDP preflight**. +11. Send known traffic to the exact address shown under **Source**. +12. Inspect source state, datagram and byte rates, last-input age, valid and malformed frames, checksum failures, message families, endpoints, and the decoder-fit assessment. +13. Select **Start recording** when the decoder fits. **Record with warning** or **Start recording anyway** remains available when the probe has traffic problems or no traffic, but the warning becomes an operator decision rather than a hidden assumption. +14. Confirm the status is **Recording**, then send the telemetry that belongs in evidence. +15. Watch **Datagrams received**, **Input bytes**, **Records retained**, **Bytes retained**, and **Bridge state**. +16. Select **Stop, save & replay**. Using UDP port `0` lets the bridge choose an available port. Read the actual bound port under **Source** before starting the sender. +UDP preflight uses a temporary bridge capture identity. Starting recording first requires that probe to stop cleanly, discards its sampled traffic, and opens a new capture identity. If NarrowsLink cannot confirm the stop, it refuses to record because the evidence boundary is not proven. The resulting `.nlsession` begins only with traffic received after **Start recording**. + For NMEA 0183, send one complete `$...*HH` sentence per UDP datagram. Concatenating multiple sentences into one datagram is not split automatically. For traffic from another machine, bind the receiving computer's interface address or `0.0.0.0`. The latter listens on every local IPv4 interface, so prefer a narrower address when possible. Firewall and routing rules still apply. @@ -194,13 +203,16 @@ Physical serial capture requires a browser with Web Serial support, normally a C 3. Enter a **Session title** and confirm the **Display timezone**. 4. Select or load the decoder pack that matches the serial stream. 5. Set **Baud rate**, **Data bits**, **Stop bits**, **Parity**, and **Flow control**. The defaults are `115200`, `8`, `1`, `None`, and `None`. -6. Select **Select port & start**. +6. Select **Select port & preflight**. 7. Choose the device in the browser's native prompt. -8. Confirm **Serial state: open** and status **Recording**. -9. Watch the serial reads, input bytes, retained records, and retained bytes. -10. Select **Stop, save & replay**. +8. Confirm **Serial state: open**, send known traffic, and inspect the preflight decoder-fit assessment. +9. Select **Start recording**, **Record with warning**, or **Start recording anyway** as the observed traffic warrants. +10. Send the telemetry that belongs in evidence and watch the serial reads, input bytes, retained records, and retained bytes. +11. Select **Stop, save & replay**. -Device selection and port setup happen before the capture clock starts. NarrowsLink retains undecodable and partial input as evidence. A disconnect or read failure produces an incomplete receipt and a capture-path diagnostic rather than silently claiming a clean capture. +Device selection and port setup happen before the capture clock starts. When recording begins, NarrowsLink keeps the selected port open but resets serial framing and counters; only subsequent reads enter the immutable session. NarrowsLink retains undecodable and partial input received after that boundary as evidence. A disconnect or read failure produces an incomplete receipt and a capture-path diagnostic rather than silently claiming a clean capture. + +Both preflight paths analyze at most 256 input units, 512 KiB of input, and 16 UDP endpoints. The dialog retains aggregate observations, not sampled payloads. **Stop preflight** closes the temporary source without creating, saving, or downloading a session. NSL-01 serial framing uses its sync word and declared binary length. NMEA serial framing uses line-feed boundaries, preserves CRLF, and retains overlong or unterminated tails as bounded partial records. @@ -279,7 +291,7 @@ Version 3 raw and decoded artifacts are each limited to 100,000 rows. When inves Treat received `.nlb` bytes as untrusted. -The current repository build can verify and open the incident directly: +The v0.2.0 application can verify and open the incident directly: 1. Start NarrowsLink on the receiving machine. 2. Select **Open evidence** in the Sessions rail or top bar. @@ -327,11 +339,9 @@ Do not extract a bundle that exits `1`. Correct path, permissions, or command us A valid bundle can truthfully report `incomplete` or `unknown` capture or provenance evidence. Version 3 bundles are unsigned, so the verifier reports authenticity as `not-established`. Exchange the reported bundle SHA-256 or expected manifest identity through a separately trusted channel when authorship or source-channel authenticity matters. -The tagged v0.1.0 package includes the CLI verifier but predates the in-application receiver. Use the current repository build until the receiver workspace is included in a later tagged release. - ## Compare two bounded inputs -The current repository build can compare an exact incident from the active replay or the fixed range from a verified receiver bundle with one candidate session or bundle. +NarrowsLink v0.2.0 can compare an exact incident from the active replay or the fixed range from a verified receiver bundle with one candidate session or bundle. 1. In the replay workspace, select the baseline incident and choose **Compare**. In the receiver workspace, choose **Compare** to use the bundle's exact included range. 2. Under **Candidate**, choose a `.nlsession`, `.json`, or `.nlb`. NarrowsLink validates a session through the normal decoder pipeline and verifies a bundle through the production receiver before continuing. Session processing shows the same phase progress as replay import and can be cancelled without replacing either source workspace. @@ -348,8 +358,6 @@ The finding includes both immutable input identities, source durations, exact ra Invalid or incompatible candidate input leaves the current replay, receiver, and any open comparison unchanged. **Return** goes back to the source workspace; **New comparison** keeps the same baseline and reopens setup. -The tagged v0.1.0 package predates comparative replay. Use the current repository build until the feature is included in a later tagged release. - ## Use the local session library The Sessions rail contains validated canonical sessions stored in IndexedDB. @@ -418,8 +426,11 @@ To intentionally purge the browser-held library and workspace, preserve any requ | The browser did not open | Keep `narrowslink serve` running and open the printed loopback URL. | | Port `47890` is occupied | Stop the existing process when possible. An alternate `--app-port` works, but it selects a different browser-storage origin. | | The managed capture status is missing or invalid | Start the installed package with `narrowslink serve`; do not serve the application directory as static files. | -| UDP capture will not start | Confirm the bind address exists locally, the port is free, and multicast group and interface values use the same IP family. | -| UDP counters remain at zero | Confirm status **Recording**, send to the exact address under **Source**, and check firewall, routing, and sender configuration. | +| UDP preflight will not start | Confirm the bind address exists locally, the port is free, and multicast group and interface values use the same IP family. | +| UDP preflight sees no traffic | Send to the exact address under **Source**, then check firewall, routing, and sender configuration. Port `0` changes to the actual assigned port after preflight opens. | +| Preflight reports a decoder mismatch | Confirm the sender's framing and protocol, then select or load the matching pack. Record with the warning only when preserving intentionally mismatched raw evidence is the test objective. | +| Recording cannot follow UDP preflight | NarrowsLink could not prove the temporary probe stopped. Select **Stop preflight**, resolve the bridge problem, and run a new preflight; do not treat probe traffic as recorded evidence. | +| A capture profile will not save | Confirm browser local storage is available and the profile set remains within 16 profiles and 2 MiB. Credentials, device permission, titles, and telemetry must be configured separately. | | A decoder pack will not load | Confirm the file is at most 512 KiB, was sealed with `narrowslink decoder seal`, uses a supported runtime, and passes its bundled fixtures. | | NMEA records are partial or unknown | Send one sentence per UDP datagram, or terminate each serial sentence with LF; confirm `$` prefix and `*HH` checksum. | | Web Serial is unavailable | Use a supported Chromium browser at the loopback application URL, or use UDP capture. | @@ -448,11 +459,13 @@ Review and sanitize evidence before committing it to a repository, attaching it The managed bridge control plane is loopback-only and uses an internal short-lived credential. The UDP listener still binds the interface chosen by the operator and can receive traffic from that interface. -Release checksums and bundle verification establish internal consistency. The v0.1 release, checksum file, and version 3 evidence bundles are unsigned. They do not establish publisher, author, source-channel, or build-environment authenticity. +Release checksums and bundle verification establish internal consistency. The v0.2 release, checksum file, decoder packs, comparison findings, and version 3 evidence bundles are unsigned. They do not establish publisher, author, source-channel, or build-environment authenticity. ## Current operating limits - Live capture supports UDP and Web Serial, not TCP or other transports. +- Capture profiles are limited to 16 entries and 2 MiB of canonical content. They store an exact decoder pack and transport settings but no credentials, device permission, session titles, or telemetry. +- Preflight analyzes at most 256 input units, 512 KiB of sampled input, and 16 UDP endpoints. It retains aggregate observations only, and preflight traffic is intentionally excluded from the recorded session. - Bundled packs support NSL-01 and checksummed NMEA 0183 GGA, RMC, and HDT. Local packs are limited to supported bounded runtimes; arbitrary code and automatic protocol detection are not supported. - Imported and saved replay documents are limited to 64 MiB of canonical UTF-8 JSON, 200,000 records, and 24 hours. - Live capture is limited to 100,000 retained records, 32 MiB of retained payload bytes, 24 hours, and a canonical file that fits the 64 MiB replay limit. diff --git a/USE_CASES.md b/USE_CASES.md index 94b69d0..f24b86d 100644 --- a/USE_CASES.md +++ b/USE_CASES.md @@ -27,10 +27,12 @@ Use-case IDs are permanent. Update an entry when its current workflow, support l **Workflow:** -1. Start NarrowsLink, choose the bundled NSL-01 or NMEA 0183 pack or load a local bounded pack, then select managed UDP or Web Serial capture and configure the source. -2. NarrowsLink validates pack identity, runtime compatibility, schema, and conformance fixtures before recording raw datagrams or assembled serial records with byte counts, monotonic offsets, malformed input, UDP remote endpoints and bridge-journal events, or the browser-exposed serial device identity and negotiated settings. -3. Stop the source with **Stop, save & replay**. -4. Save the downloaded version 2 `.nlsession`, follow worker processing through the same validation and decoder path as import, continue into replay with the validated finalized session, and retain the canonical session in the local library when IndexedDB succeeds. +1. Start NarrowsLink, apply or create a local capture profile, or directly choose the bundled NSL-01 or NMEA 0183 pack or load a bounded local pack; then select managed UDP or Web Serial and configure the source. +2. Run transport preflight with known traffic. NarrowsLink validates pack identity, runtime compatibility, schema, and conformance fixtures, then reports bounded traffic, framing, checksum, family, and endpoint observations without retaining sampled payloads as session evidence. +3. Deliberately start recording. UDP stops and discards the probe before opening a new capture identity; serial keeps the selected port but resets framing so only future reads cross the evidence boundary. +4. Record raw datagrams or assembled serial records with byte counts, monotonic offsets, malformed input, UDP remote endpoints and bridge-journal events, or the browser-exposed serial device identity and negotiated settings. +5. Stop the source with **Stop, save & replay**. +6. Save the downloaded version 2 `.nlsession`, follow worker processing through the same validation and decoder path as import, continue into replay with the validated finalized session, and retain the canonical session in the local library when IndexedDB succeeds. **Radio transport capture topologies:** @@ -38,16 +40,16 @@ NarrowsLink starts where telemetry bytes are available to the laptop as UDP data | Option | Topology | What NarrowsLink observes | Test path | | --- | --- | --- | --- | -| 1 | USB serial radio connected to the laptop | The radio terminates the RF link and exposes telemetry bytes through a browser-selected serial device. NarrowsLink applies the selected pack's bounded serial framing, records serial reads and retained bytes, preserves device identity when exposed and negotiated settings, and retains malformed input plus disconnect or read-failure evidence. | Use a Chromium browser, select the matching decoder pack and **Serial port**, choose the USB serial radio, confirm **Serial state: open**, send known traffic, stop with **Stop, save & replay**, and verify retained records plus decoded or diagnostic output. | -| 2 | Radio or base station outputs UDP to the laptop | The external receiver terminates the RF link and forwards payloads as UDP datagrams to the NarrowsLink bridge. NarrowsLink records datagrams, byte counts, remote endpoint attribution, bridge journal entries, retained records, and capture-path diagnostics. | Start UDP capture on a known bind host and port, configure the base station to send to that exact laptop address and port, confirm datagram and retained-record counters advance, stop with **Stop, save & replay**, then replay and export a small evidence bundle. | -| 3 | Ground-control software forwards a UDP copy | Another application remains the primary receiver, and NarrowsLink receives a duplicated stream. NarrowsLink can preserve and verify the copy it received, but it cannot prove losses that happened before the copy point unless the forwarded payload or side channel carries those counters. | Configure the ground-control station or a small proxy to tee the telemetry to NarrowsLink's UDP address, run the normal UDP capture path, and compare NarrowsLink counters with the upstream tool's send or receive counters when available. | -| 4 | Network multicast observer | The sender publishes UDP to a multicast group and port; NarrowsLink joins that group on the selected interface. This is an IP-network observer pattern, not passive RF capture. NarrowsLink records multicast bind configuration, datagrams, remote endpoints, bridge journal evidence, and retained records. | Select the matching decoder pack, start UDP capture with a multicast group and matching bind/interface family, send known datagrams to the group and port, confirm NarrowsLink receives them without being the sender's only destination, then stop, replay, and verify provenance. | +| 1 | USB serial radio connected to the laptop | The radio terminates the RF link and exposes telemetry bytes through a browser-selected serial device. NarrowsLink applies the selected pack's bounded serial framing, records serial reads and retained bytes, preserves device identity when exposed and negotiated settings, and retains malformed input plus disconnect or read-failure evidence. | Use a Chromium browser, select the matching decoder pack and **Serial port**, choose **Select port & preflight**, send known traffic, inspect framing and decoder fit, select **Start recording**, send the test evidence, stop with **Stop, save & replay**, and verify retained records plus decoded or diagnostic output. | +| 2 | Radio or base station outputs UDP to the laptop | The external receiver terminates the RF link and forwards payloads as UDP datagrams to the NarrowsLink bridge. NarrowsLink records datagrams, byte counts, remote endpoint attribution, bridge journal entries, retained records, and capture-path diagnostics. | Configure a known bind host and port, choose **Run UDP preflight**, send known base-station traffic to the exact reported address, inspect endpoint and decoder fit, select **Start recording**, send the test evidence, stop with **Stop, save & replay**, then replay and export a small evidence bundle. | +| 3 | Ground-control software forwards a UDP copy | Another application remains the primary receiver, and NarrowsLink receives a duplicated stream. NarrowsLink can preserve and verify the copy it received, but it cannot prove losses that happened before the copy point unless the forwarded payload or side channel carries those counters. | Configure the ground-control station or a small proxy to tee telemetry to NarrowsLink's UDP address, prove the copy path in preflight, deliberately start recording, and compare NarrowsLink counters with the upstream tool's send or receive counters when available. | +| 4 | Network multicast observer | The sender publishes UDP to a multicast group and port; NarrowsLink joins that group on the selected interface. This is an IP-network observer pattern, not passive RF capture. NarrowsLink records multicast bind configuration, datagrams, remote endpoints, bridge journal evidence, and retained records. | Select the matching decoder pack, run UDP preflight with a multicast group and matching bind/interface family, send known datagrams to the group and port, confirm NarrowsLink observes them without being the sender's only destination, then start recording, stop, replay, and verify provenance. | For multicast, the sender addresses one UDP datagram stream to a multicast group such as `239.42.91.4:9104` instead of one specific host. Receivers that join the group on the relevant network interface can receive the stream at the same time. This is useful when a base station, ground-control tool, and NarrowsLink all need the same telemetry without configuring separate unicast destinations. It still depends on network equipment, operating-system multicast support, firewall rules, interface selection, and matching IPv4 or IPv6 family between bind address and multicast group. -**Current constraints:** The tagged v0.1.0 package predates decoder packs; the current repository build requires Node.js 20.19 or newer and a local browser. `narrowslink serve` gives the browser a same-origin relay to the managed loopback bridge and keeps its bearer credential internal, without asking the operator to transfer it; source-development mode retains a separate manual bearer-token bridge. Serial capture requires a browser with Web Serial support and a secure context such as `localhost` or `127.0.0.1`. The cross-browser automated serial gate injects the public browser API; it proves the complete application path but does not certify native device choosers, USB drivers, operating-system disconnect behavior, or physical adapters. Packs are limited to supported bounded runtimes; arbitrary JavaScript and automatic protocol detection are not accepted. NMEA UDP requires one sentence per datagram, while serial NMEA uses line-feed boundaries. Live capture is limited to 100,000 retained records, 32 MiB of retained payload bytes, 24 hours, and a canonical file within the 64 MiB replay limit. If IndexedDB, Web Crypto, or the available quota prevents library persistence, the downloaded file and active replay remain usable, but NarrowsLink reports that the session was not saved in the library. +**Current constraints:** NarrowsLink v0.2.0 requires Node.js 20.19 or newer and a local browser. `narrowslink serve` gives the browser a same-origin relay to the managed loopback bridge and keeps its bearer credential internal, without asking the operator to transfer it; source-development mode retains a separate manual bearer-token bridge. Capture profiles are local convenience state limited to 16 entries and 2 MiB; they contain the exact pack and transport settings but no credential, device permission, title, or telemetry. Preflight retains aggregate observations only and is bounded to 256 input units, 512 KiB, and 16 UDP endpoints. Probe traffic is intentionally excluded from evidence. Serial capture requires a browser with Web Serial support and a secure context such as `localhost` or `127.0.0.1`. The cross-browser automated serial gate injects the public browser API; it proves the complete application path but does not certify native device choosers, USB drivers, operating-system disconnect behavior, or physical adapters. Packs are limited to supported bounded runtimes; arbitrary JavaScript and automatic protocol detection are not accepted. NMEA UDP requires one sentence per datagram, while serial NMEA uses line-feed boundaries. Live capture is limited to 100,000 retained records, 32 MiB of retained payload bytes, 24 hours, and a canonical file within the 64 MiB replay limit. If IndexedDB, Web Crypto, or the available quota prevents library persistence, the downloaded file and active replay remain usable, but NarrowsLink reports that the session was not saved in the library. -**Implementation evidence:** [decoder-pack contract](src/domain/decoder-pack.ts), [conformance runner](src/domain/decoder-conformance.ts), [capture dialog](src/capture/CaptureDialog.tsx), [bounded recorder](src/capture/recorder.ts), [runtime-selected serial assembly](src/capture/serial-assembler.ts), [UDP client](src/capture/udp-bridge.ts), [Web Serial adapter](src/capture/web-serial.ts), [durable session library](src/storage/session-library.ts), [decoder-pack and persistence tests](src/domain/decoder-pack.test.ts), [capture-pipeline tests](src/capture/capture-pipeline.test.ts), the [cross-browser real-UDP NSL-01 and NMEA capture gate](tests/e2e/capture-to-evidence.spec.ts), the [cross-browser simulated-serial capture-to-evidence gate](tests/e2e/serial-capture-to-evidence.spec.ts), and [browser storage/failure recovery](tests/e2e/storage-failures.spec.ts). +**Implementation evidence:** [decoder-pack contract](src/domain/decoder-pack.ts), [conformance runner](src/domain/decoder-conformance.ts), [capture profiles](src/capture/capture-profile.ts), [bounded preflight](src/capture/capture-preflight.ts), [capture dialog](src/capture/CaptureDialog.tsx), [bounded recorder](src/capture/recorder.ts), [runtime-selected serial assembly](src/capture/serial-assembler.ts), [UDP client](src/capture/udp-bridge.ts), [Web Serial adapter](src/capture/web-serial.ts), [durable session library](src/storage/session-library.ts), [profile tests](src/capture/capture-profile.test.ts), [preflight tests](src/capture/capture-preflight.test.ts), [decoder-pack and persistence tests](src/domain/decoder-pack.test.ts), [capture-pipeline tests](src/capture/capture-pipeline.test.ts), the [cross-browser real-UDP NSL-01, NMEA, profile, mismatch, and evidence-boundary gate](tests/e2e/capture-to-evidence.spec.ts), the [cross-browser simulated-serial preflight-to-evidence gate](tests/e2e/serial-capture-to-evidence.spec.ts), and [browser storage/failure recovery](tests/e2e/storage-failures.spec.ts). ## UC-002 — Investigate a recorded telemetry fault @@ -102,7 +104,7 @@ For multicast, the sender addresses one UDP datagram stream to a multicast group **Current constraints:** NarrowsLink does not infer synchronized clocks, discover matching events, establish causality, or claim that higher traffic or an arbitrary decoded value is better. Range-start alignment means only that both selected starts are treated as relative zero; shared-event alignment depends on operator-supplied exact anchors inside both half-open ranges. Packet, diagnostic, and decoded-field deltas require the exact same decoder, schema, pack, and runtime identity plus selected raw support in both inputs. RSSI deltas require one matching evidence basis, and decoded-packet RSSI also requires the same decoder identity. Capture evidence is assessed separately and can remain review-required, incomplete, unknown, or unavailable. A bundle exposes only the evidence it carries. Session candidates share the 64 MiB, 200,000-record, 24-hour replay envelope; comparison construction projects only the selected ranges, but both active inputs and the bounded result remain in browser memory. The finding is bounded to 1 MiB, cites but does not embed either source, and is unsigned; its canonical SHA-256 catches content alteration but does not prove authorship, source-channel authenticity, or the cause of a measured difference. The current UI exports findings but does not import them as a case file. -**Implementation evidence:** [comparison contract, metrics, and finding validator](src/domain/comparison.ts), [comparison workspace](src/comparison/ComparisonWorkspace.tsx), [worker comparison processor](src/processing/comparison-processing.ts), [comparison domain tests](src/domain/comparison.test.ts), the [controlled real-UDP comparison and finding gate](tests/e2e/comparison-workspace.spec.ts), the [maximum-record comparison and bundle gate](tests/e2e/large-session-processing.spec.ts), [comparison accessibility and responsive coverage](tests/e2e/accessibility.spec.ts), and the [unpacked-release receiver-to-session comparison gate](tests/release/capture-to-evidence.spec.ts). Decoder and pipeline regressions remain backed by the [fixture generator](scripts/generate-demo-session.mjs), [conformance runner](src/domain/decoder-conformance.ts), [decoder-pack tests](src/domain/decoder-pack.test.ts), [pack CLI tests](scripts/narrowslink-decoder.test.ts), and [fixture integration tests](src/domain/fixture.integration.test.ts). +**Implementation evidence:** [comparison contract, metrics, and finding validator](src/domain/comparison.ts), [comparison workspace](src/comparison/ComparisonWorkspace.tsx), [worker comparison processor](src/processing/comparison-processing.ts), [comparison domain tests](src/domain/comparison.test.ts), the [controlled real-UDP comparison and finding gate](tests/e2e/comparison-workspace.spec.ts), the [maximum-record comparison and bundle gate](tests/e2e/large-session-processing.spec.ts), [comparison accessibility and responsive coverage](tests/e2e/accessibility.spec.ts), and the [unpacked-release receiver-to-session comparison gate](tests/release/capture-to-evidence.spec.ts). Decoder and pipeline regressions remain backed by the [fixture generator](scripts/generate-demo-session.mjs), [conformance runner](src/domain/decoder-conformance.ts), [decoder-pack tests](src/domain/decoder-pack.test.ts), [pack CLI tests](scripts/narrowslink-decoder.test.mjs), and [fixture integration tests](src/domain/fixture.integration.test.ts). ## UC-005 — Hand off a verifiable incident bundle @@ -119,7 +121,7 @@ For multicast, the sender addresses one UDP datagram stream to a multicast group 5. Review internal consistency, evidence completeness, and source authenticity as separate claims, then inspect the exact included timeline, rows, diagnostics, source annotations, transport evidence, decoder identity, and explicit unavailable groups. 6. Record any receiver-owned finding in the separate **Notes** tab and compare the bundle and pack identities through a separately trusted channel when authenticity matters. -**Current constraints:** The application receiver and CLI accept bounded version 3 `.nlb` bundles and treat their ZIP structure and contents as untrusted input. Raw NDJSON and decoded CSV artifacts are each limited to 100,000 rows, so a replay above that count requires a narrower selected incident when those groups are included. The in-application receiver is currently under `[Unreleased]` and is not in the tagged v0.1.0 package. The receiver reconstructs only the selected evidence carried by the archive; it does not fabricate excluded artifacts or whole-session context. When the exact pack is available, the verifier validates pack identity and fixtures and reproduces decoded rows from selected raw records. Excluding the schema artifact can prevent replay-checking a local pack and produces an explicit warning. A passing result establishes internal consistency, not complete capture evidence: a valid bundle can truthfully report `incomplete` or `unknown` capture or provenance evidence. Version 3 bundles and decoder packs are unsigned, so the verifier reports authenticity as not established and cannot prove who created them or whether the originating build was trustworthy. Receiver findings are local browser records keyed by the exact bundle SHA-256; they are not embedded in, signed with, or exported from the source archive. A receiver can start UC-004 from the verified bundle, but its separately stored note is not copied into the comparison; only a conclusion explicitly authored in that workspace is exported in the separate `.nlcompare.json`. Raw telemetry, coordinates, identifiers, and notes may be sensitive and must be reviewed before sharing. +**Current constraints:** The application receiver and CLI accept bounded version 3 `.nlb` bundles and treat their ZIP structure and contents as untrusted input. Raw NDJSON and decoded CSV artifacts are each limited to 100,000 rows, so a replay above that count requires a narrower selected incident when those groups are included. The receiver reconstructs only the selected evidence carried by the archive; it does not fabricate excluded artifacts or whole-session context. When the exact pack is available, the verifier validates pack identity and fixtures and reproduces decoded rows from selected raw records. Excluding the schema artifact can prevent replay-checking a local pack and produces an explicit warning. A passing result establishes internal consistency, not complete capture evidence: a valid bundle can truthfully report `incomplete` or `unknown` capture or provenance evidence. Version 3 bundles and decoder packs are unsigned, so the verifier reports authenticity as not established and cannot prove who created them or whether the originating build was trustworthy. Receiver findings are local browser records keyed by the exact bundle SHA-256; they are not embedded in, signed with, or exported from the source archive. A receiver can start UC-004 from the verified bundle, but its separately stored note is not copied into the comparison; only a conclusion explicitly authored in that workspace is exported in the separate `.nlcompare.json`. Raw telemetry, coordinates, identifiers, and notes may be sensitive and must be reviewed before sharing. **Implementation evidence:** [shared evidence contract](src/domain/evidence-contract.ts), [bundle builder](src/domain/bundle.ts), [worker bundle processor](src/processing/evidence-bundle-processing.ts), [production receiver verifier](verifier/evidence-verifier.ts), [bounded ZIP reader](verifier/evidence-zip.ts), [browser receiver loader](src/receiver/load-evidence-bundle.ts), [bounded receiver document](src/receiver/receiver-document.ts), [receiver workspace](src/receiver/ReceiverWorkspace.tsx), [separate receiver findings](src/receiver/receiver-storage.ts), [receiver and pack CLI](scripts/narrowslink.ts), [bundle tests](src/domain/bundle.test.ts), [receiver verifier and document tests](verifier/evidence-verifier.test.ts), the [cross-browser NSL-01 and NMEA capture-to-receiver gate](tests/e2e/capture-to-evidence.spec.ts), the [maximum-record cancellation and verified-bundle gate](tests/e2e/large-session-processing.spec.ts), [receiver accessibility and reflow coverage](tests/e2e/accessibility.spec.ts), and the [artifact-local receiver and replacement gate](tests/release/). diff --git a/design-qa.md b/design-qa.md index 114a22f..dec495a 100644 --- a/design-qa.md +++ b/design-qa.md @@ -11,15 +11,18 @@ This document records the currently accepted visual baseline and its verificatio - Accepted focused source/geometry comparison: [focused comparison](docs/design/comparison-production-focused.png) - Responsive session-library dialog evidence: [responsive library implementation](docs/design/implementation-functional-mobile.png) - Capture-integrity functional evidence: [incomplete UDP capture replay](docs/design/implementation-capture-integrity.png) -- Decoder-pack capture setup evidence: [NMEA capture setup](docs/design/live-capture-setup.jpg) +- Field-capture setup evidence: [capture profile setup](docs/design/capture-profile-setup.png) +- Field-capture preflight evidence: [verified UDP preflight](docs/design/capture-preflight-ready.png) +- Responsive field-capture evidence: [mobile capture setup](docs/design/capture-profile-mobile.png) - Received-incident evidence workspace: [verified receiver workspace](docs/design/receiver-workspace.png) - Comparative replay evidence workspace: [bounded comparison workspace](docs/design/comparison-workspace.png) -- Desktop viewport: `1487 × 1058`, matching the source image. -- Responsive viewport: `390 × 844`; measured document width and scroll width both equal `390 px`, and the open library and live-capture dialogs each measure `358 px` wide. +- Desktop viewport: `1487 × 1058`, matching the `1487 × 1058` source image at device scale factor `1`; the capture-profile and preflight implementation images are also `1487 × 1058` at device scale factor `1`, so no density normalization was required. +- Responsive viewport: `390 × 844` at device scale factor `1`; measured document, body scroll, and document scroll widths all equal `390 px`, and the open live-capture dialog measures `358 × 812 px`. - State: bundled Harbor relay replay reopened from the library; replay paused inside the link-fade incident at `23:40`; Narrative tab; all six evidence groups selected; two genuine saved sessions; two local operator markers, one in the visible timeline; and a session-wide note present. - Release-gate state: the same bundled incident and operator note with one genuine saved session, visible non-color diagnostic severity tokens, selected-incident semantics, and all six evidence groups selected. - Comparison evidence state: the selected link-fade incident is compared with the same validated fixture at `1280 × 720` using explicit range-start alignment. Packet, diagnostic, decoded-field, and link evidence are comparable and unchanged; the overall result remains unresolved because both legacy-v1 capture-integrity assessments are unknown. The controlled real-capture regression is proven separately by the browser gate. - Maximum-record evidence state: a generated 52,378,445-byte, 200,000-record replay opens in the unchanged mission workspace while import, saved-session reopen, candidate loading, comparison construction, and bundle construction use a compact modal with a named progress bar, current phase, percentage, and cancel action. The transient surface preserves the existing square, restrained instrument language and never displaces the active workspace until processing succeeds. +- Field-capture evidence state: the packaged managed bridge opened an ephemeral loopback socket, observed 24 real NSL-01 datagrams and 737 bytes from one endpoint, reported 24 valid frames with no malformed frames, identified Position and Attitude families, confirmed decoder fit, and kept the explicit preflight-to-recording boundary beside a persistent **Start recording** action. ### Current release result @@ -47,13 +50,21 @@ This document records the currently accepted visual baseline and its verificatio [![NarrowsLink comparing two explicitly aligned telemetry incidents](docs/design/comparison-workspace.png)](docs/design/comparison-workspace.png) +### Accepted field-capture result + +[![NarrowsLink field setup with reusable capture profiles](docs/design/capture-profile-setup.png)](docs/design/capture-profile-setup.png) + +[![NarrowsLink confirming UDP source traffic and decoder fit before evidence recording](docs/design/capture-preflight-ready.png)](docs/design/capture-preflight-ready.png) + +[![NarrowsLink field setup at 390 by 844 pixels](docs/design/capture-profile-mobile.png)](docs/design/capture-profile-mobile.png) + ## Final findings No actionable P0, P1, or P2 visual findings remain. The final implementation matches the source's primary composition and geometry: `232 px` source rail, compact session command bar, stacked overview plots, shared time grid, labeled telemetry lanes, `280 px` incident rail, amber half-open incident selection, and the full-width evidence workspace. The interface also retains square controls, quiet one-pixel structure, restrained warm-black surfaces, dense instrument typography, semantic chart colors, and a pale-blue export action. -Six visible differences are intentional and accepted product, data, or accessibility constraints: +Seven visible differences are intentional and accepted product, data, or accessibility constraints: - The source mocks several active and recent sessions. The implementation shows one genuine loaded source plus two genuinely persisted sessions and does not present invented sessions as available data. - The source shows a live-follow control. A recorded session truthfully exposes replay controls, while live capture remains available from the source rail and command bar. @@ -61,6 +72,13 @@ Six visible differences are intentional and accepted product, data, or accessibi - Evidence rows describe the real local NarrowsLink archive contents and sizes rather than the source's illustrative PCAP and `24.7 MB` copy. - The evidence table has a sixth required Capture integrity row so optional derived diagnostics remain independently selectable while the transport event log, provenance document, bridge journal, and receipt cannot be removed from a verifiable archive. - Diagnostics add visible severity words or compact `C`/`W`/`I` tokens, and overview incidents expose selected-state treatment, so meaning is not carried by color alone. +- Live capture adds a compact local profile row and a bounded preflight state before recording. These controls extend the source-aligned dialog rather than changing the mission workspace, and their locked, warning, and ready states represent real source observations rather than decorative setup progress. + +## Comparison history + +- Initial field-capture pass at `1487 × 1058`: adding the profile row pushed the primary capture actions below the visible modal edge, a P2 reachability and source-geometry regression. +- Fix: made the capture action strip sticky inside the existing scroll-bounded, square-cornered dialog and corrected the narrow layout so **Save setup** and delete occupy separate grid tracks. +- Post-fix evidence: [desktop capture setup](docs/design/capture-profile-setup.png), [ready preflight](docs/design/capture-preflight-ready.png), and [mobile capture setup](docs/design/capture-profile-mobile.png) keep the current phase action visible without document overflow or control overlap. No actionable P0, P1, or P2 findings remain. ## Focused comparison evidence @@ -71,7 +89,7 @@ Six visible differences are intentional and accepted product, data, or accessibi - Session library: the current desktop and responsive evidence confirms two real IndexedDB entries, active-row treatment, meaningful date/duration/integrity metadata, guarded removal, a reachable narrow-screen dialog, and no horizontal body overflow. - Capture integrity: the functional evidence confirms that an incomplete v2 receipt is visible in session context, its immutable UDP anomaly appears in the shared Diagnostics lane and Narrative as `Capture path`, and the evidence workspace keeps Capture integrity mandatory while Diagnostics remains optional. - Transport provenance: a verified loopback capture confirms that the source-aligned incident rail can expose capture identity, bound socket, endpoint attribution, bridge totals, explicit unavailable kernel-drop evidence, lifecycle journal entries, and evidence boundaries without disturbing the mission timeline's density or hierarchy. -- Decoder portability: the current capture setup keeps the decoder decision ahead of transport setup, exposes the selected pack's runtime and shortened immutable identity, and preserves the established square, compact instrument language. At `390 × 844`, the selector and load action stack inside the `358 px` dialog without horizontal overflow. +- Field capture: the current setup keeps profile recall, decoder identity, and transport configuration in that order; exposes live source, rate, last-input, validity, family, endpoint, and decoder-fit observations; states that probe bytes are excluded from evidence; and keeps the current transition action visible. At `390 × 844`, profile, decoder, transport, and sticky actions fit inside the `358 px` dialog without overlap or horizontal overflow. - Received evidence: the accepted receiver keeps the same rail, compact command bar, warm-black one-pixel structure, dense instrument typography, shared timeline vocabulary, and inspector geometry while giving verification claims their own fixed band. Artifact counts, exact range, packet integrity, diagnostics, source evidence, and explicit unavailable states all come from the verified archive rather than decorative values. - Comparative replay: the accepted comparison retains the same instrument language while replacing the mission timeline with two explicitly aligned source lanes, a comparability strip, bounded metric table, and fixed trace inspector. Input identities, range boundaries, overlap, unmatched tails, evidence IDs, and unresolved states come from the comparison contract rather than visual inference. At `390 × 844`, commands wrap into a stable two-column group, the eligibility matrix becomes two columns, timeline detail remains internally bounded, and document and body widths both remain `390 px`. - Long-running processing: the modal uses the existing one-pixel structure, dense type scale, compact status hierarchy, and familiar secondary cancel action. Phase text and a native progress value communicate change without relying on animation or color, and the underlying validated workspace remains visually stable throughout cancellation. @@ -79,10 +97,10 @@ Six visible differences are intentional and accepted product, data, or accessibi ## Required fidelity surfaces - Fonts and typography: bundled Inter carries interface copy and IBM Plex Mono carries times, values, and protocol metadata. Uppercase micro-labels, numeric alignment, weight, and dense line heights were checked in the focused comparison. -- Spacing and layout rhythm: the viewport, rail and incident-panel widths, header and overview heights, label/scale gutters, lane baselines, evidence proportions, dividers, and square corners were compared against the source at identical dimensions. +- Spacing and layout rhythm: the viewport, rail and incident-panel widths, header and overview heights, label/scale gutters, lane baselines, evidence proportions, dividers, square corners, capture-profile grid, scroll-bounded dialog, and sticky action strip were compared at the stated desktop and mobile dimensions. - Colors and visual tokens: warm near-black surfaces, muted gray structure, amber incident selection, green link/position data, blue received packet rate, red inferred missing frames, purple markers, cyan packet-family data, and pale-blue primary actions preserve the source semantics. - Image and icon fidelity: the repository's NarrowsLink mark is reused as a real image asset. The source contains no photography or illustration. Recharts renders data plots and Phosphor supplies the icon family; no placeholder art, emoji, or improvised CSS/SVG illustration was introduced. -- Copy and content: session metadata, diagnostics, units, decoder state, bundle contents, and privacy language remain coherent and derived from the same replay pipeline instead of reproducing contradictory prototype numbers. +- Copy and content: session metadata, diagnostics, units, decoder state, bundle contents, profile exclusions, source observations, decoder-fit guidance, and preflight evidence boundary remain coherent and derived from application state instead of reproducing contradictory prototype numbers. - Responsiveness: at `390 × 844`, the page has no horizontal body overflow, labels no longer collide, the command strip and Saved control remain reachable, the `358 px` library dialog fits the viewport, the telemetry surface uses its deliberate internal scroller, and panels preserve the desktop hierarchy. - Accessibility review: semantic buttons, native checkboxes/selects, labels, tabs, dialogs, named progress bars, cancellation controls, focus treatment, status text, chart summaries, disabled states, reduced-motion behavior, non-color diagnostic cues, and narrow keyboard scrollers pass axe rules tagged WCAG A/AA plus interaction coverage in Playwright Chromium, Firefox, and WebKit. All three engines pass `960`, `640` (`200%`-equivalent), and `390` CSS-pixel reflow plus forced-color checks. This is not a claim of full accessibility compliance; packaged-browser screen-reader, native zoom, and hardware matrices remain manual follow-up work documented in [ACCESSIBILITY.md](ACCESSIBILITY.md). @@ -100,6 +118,11 @@ Six visible differences are intentional and accepted product, data, or accessibi - Built and downloaded `capture-integrity-browser-review-capture-integrity-incident.nlb`; the success state reported six selected groups and verifiable local evidence with Capture integrity locked on while Diagnostics remained independently selectable. - Opened and closed the live UDP/serial capture setup. - Selected the bundled NMEA pack and confirmed its runtime and pack identity updated together; the same controls reflowed without horizontal overflow at `390 × 844`. +- Saved, updated, reapplied, and removed a local capture profile while confirming the exact custom decoder pack and transport settings returned and bridge credentials, session title, device permission, and telemetry did not. +- Ran real UDP preflight with matching NSL-01 traffic, confirmed 24 valid frames and the observed endpoint, then proved probe records were absent after the new owned recording began. +- Ran a deliberate NMEA-decoder/NSL-01-traffic mismatch, confirmed the explicit warning path remained available, and stopped the probe without creating or saving a session. +- Ran simulated Web Serial preflight, retained one selected and opened device, reset framing at **Start recording**, and proved only reads after that boundary entered the session. +- Stopped preflight and confirmed the dialog returned to a zero-duration **Ready** state with an explicit no-session notice. - Saved the bundled replay, reloaded the page, and confirmed its genuine IndexedDB row and active state survived browser navigation. - Imported the exact same fixture and confirmed content-addressed deduplication kept the library at one entry and preserved the original saved order. - Added a second validated session through the real repository path, confirmed newest-first ordering, reopened it from the rail, switched away and back, and verified its per-session operator note restored. @@ -118,11 +141,11 @@ Six visible differences are intentional and accepted product, data, or accessibi - Verified the comparison setup and workspace with axe rules tagged WCAG A/AA, keyboard-scrollable evidence regions, and no page-level overflow at `960 × 900`, `640 × 900`, and `390 × 844` in Chromium, Firefox, and WebKit. - Recorded a 24-datagram loopback acceptance capture and visually confirmed the verified Provenance tab reconciled all 24 records and 737 retained bytes to one exact remote endpoint and a clean two-entry bridge journal while preserving the unavailable host-drop-counter boundary. - Exercised simulated Web Serial device selection through four fragmented reads, retained four complete frames and one terminal partial frame without byte loss, saved and deduplicated the canonical v2 session, reopened and replayed it from IndexedDB, authored an exact half-open range, and verified the downloaded `.nlb` with the production receiver in Chromium, Firefox, and WebKit. Physical device, driver, and native permission behavior remains a manual boundary. -- Launched the unpacked v0.1 operator package with `narrowslink serve` and visually confirmed the production workspace and capture dialog at the managed origin. The dialog showed the authenticated managed-bridge state and exact version/commit identity without bridge URL or token fields, while preserving the accepted component geometry and producing no browser warnings or errors. +- Launched the v0.2 operator build with `narrowslink serve` and visually confirmed the production workspace, profile setup, and live preflight at the managed origin. The dialog showed the authenticated managed-bridge state without bridge URL or token fields, preserved the accepted component geometry, and produced no browser warnings or errors. - Repeated the capture-to-receiver, replay/library, failure-recovery, axe rules tagged WCAG A/AA, focus-handoff, responsive, keyboard-scroller, and forced-color gates in Playwright Chromium, Firefox, and WebKit. - Imported the deterministic 52,378,445-byte, 200,000-record corpus with visible phase progress, retained interaction heartbeats, exact canonical-byte persistence, and no workspace replacement on cancellation; then reopened it, compared its exact 10,000-record incident, cancelled one bundle build without a download, rebuilt it, and verified exactly 10,000 raw and decoded records through the production receiver in Chromium, Firefox, and WebKit. - Repeated the maximum-record import, persistence, reload, reopen, and progress path from the independently built unpacked operator distribution in Chromium, Firefox, and WebKit. -- Ran the complete `npm run check` command set: TypeScript validation, `278` tests across `35` files, the production application and CLI builds, `49` passing source-workspace Playwright checks with `2` intentional engine-redundant cancellation skips, two byte-identical release compilations, and all `6` unpacked-release capture-to-receiver, comparison, and maximum-record checks across Chromium, Firefox, and WebKit passed. +- Current release evidence: TypeScript validation, `289` tests across `37` files, the production application and CLI builds, CLI smoke verification, `52` passing source-workspace Playwright checks with `2` intentional engine-redundant cancellation skips, two byte-identical release compilations, and all `6` unpacked-package capture-to-receiver and maximum-record checks across Chromium, Firefox, and WebKit passed. ## Implementation checklist @@ -133,9 +156,10 @@ Six visible differences are intentional and accepted product, data, or accessibi - [x] Preserve capture-path anomalies, endpoint or device provenance, bridge journal, and terminal integrity receipt through replay, operator range projection, and checksummed export. - [x] Populate the source rail with real durable sessions and preserve validated reopen, explicit removal, and responsive access. - [x] Preserve exact source identities, explicit alignment, bounded comparability, evidence traceability, and unresolved states through comparative replay and checksummed finding export. +- [x] Preserve exact profile setup, bounded non-evidence preflight, and an explicit UDP or serial transition into immutable recording. - [x] Keep maximum-record import, reopen, selected-range comparison, and bundle construction responsive, cancellable, deterministic, and free of partial persistence or downloads. - [x] Gate the full capture-to-evidence loop, independent archive verification, storage failures, keyboard focus, and automated accessibility across the supported browser-engine matrix. - [x] Verify desktop, responsive, and primary interaction states. - [x] Resolve every P0, P1, and P2 visual QA finding. -**Result: passed.** +final result: passed diff --git a/docs/design/capture-preflight-ready.png b/docs/design/capture-preflight-ready.png new file mode 100644 index 0000000000000000000000000000000000000000..d947a7e62e42175611725848d8e85294170e4a1d GIT binary patch literal 128097 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 !eooLl%r%Fs8xw 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 )&md_2j)-tDCs2ngt;uks8xmxHQzJAf|e1^ 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 BYCiw~ literal 0 HcmV?d00001 diff --git a/docs/design/capture-profile-setup.png b/docs/design/capture-profile-setup.png new file mode 100644 index 0000000000000000000000000000000000000000..7fc536e1b4115998a656d65f94e705b2a6741f30 GIT binary patch literal 125277 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 literal 0 HcmV?d00001 diff --git a/docs/releases/v0.2.0.md b/docs/releases/v0.2.0.md new file mode 100644 index 0000000..6362544 --- /dev/null +++ b/docs/releases/v0.2.0.md @@ -0,0 +1,45 @@ +# NarrowsLink v0.2.0 + +NarrowsLink v0.2.0 moves the full protocol-portable investigation and handoff workflow into the packaged operator release, then makes field setup repeatable before evidence recording begins. + +## What changed + +- Save bounded local capture profiles containing transport settings and the exact decoder pack. Profiles exclude bridge credentials, browser device permission, session names, and telemetry payloads. +- Run live UDP or Web Serial preflight before recording. Preflight reports source state, traffic and byte rate, last-input age, valid and malformed frames, checksum failures, message families, and UDP endpoints. +- Establish an explicit evidence boundary after preflight. UDP stops and discards the probe before opening a new owned capture ID; serial keeps the selected port open, resets framing, and routes only future reads to the recorder. +- Diagnose traffic-without-decoder-fit before capture while preserving an operator escape hatch to record malformed or unexpected evidence with a visible warning. +- Package the decoder-pack architecture, NMEA 0183 support, in-application evidence receiver, bounded comparative replay, cancellable worker processing, and 200,000-record replay envelope introduced since v0.1.0. +- Preserve v0.1 session, library, workspace, and evidence compatibility. Existing browser data remains tied to the same loopback origin and browser profile. + +## Release assets + +- `narrowslink-0.2.0.tgz` +- `narrowslink-0.2.0.release.json` +- `narrowslink-0.2.0.cdx.json` +- `SHA256SUMS` + +Verify the checksum set before installation: + +```bash +shasum -a 256 -c SHA256SUMS +``` + +On GNU/Linux, use `sha256sum -c SHA256SUMS`. + +Install without lifecycle scripts: + +```bash +npm install --global ./narrowslink-0.2.0.tgz --ignore-scripts +narrowslink version --json +narrowslink serve +``` + +Compare the reported version and commit with `narrowslink-0.2.0.release.json`. Keep the default `http://127.0.0.1:47890` origin and the same browser profile to retain access to the existing local session library and operator workspace. + +## Verification boundary + +The release workflow builds the package twice from the annotated tag, requires byte-identical assets, runs the complete source gate, and exercises the unpacked package through live UDP preflight, capture, replay, evidence export, receiver verification, comparison, persistence, recovery, and removal in Chromium, Firefox, and WebKit. + +Automated Web Serial coverage uses the public browser API with an injected device. Physical USB adapters, drivers, native permission prompts, operating-system disconnect behavior, manual screen-reader combinations, and actual multicast network infrastructure remain separate manual acceptance boundaries. + +Checksums and canonical identities establish internal consistency. NarrowsLink v0.2.0 artifacts, decoder packs, sessions, bundles, and comparison findings remain unsigned and do not independently establish publisher, operator, source-channel, or build-environment authenticity. diff --git a/package-lock.json b/package-lock.json index c759842..a643716 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "narrowslink", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "narrowslink", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "@fontsource/ibm-plex-mono": "^5.2.7", diff --git a/package.json b/package.json index d49e379..f6728e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "narrowslink", - "version": "0.1.0", + "version": "0.2.0", "private": true, "license": "MIT", "type": "module", diff --git a/scripts/cli-smoke.mjs b/scripts/cli-smoke.mjs index bfba807..39f3cf1 100644 --- a/scripts/cli-smoke.mjs +++ b/scripts/cli-smoke.mjs @@ -4,6 +4,8 @@ import { join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; const projectRoot = resolve(import.meta.dirname, ".."); +const packageDocument = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8")); +const expectedVersion = packageDocument.version; const outputDirectory = join(projectRoot, "dist-cli"); const cliPath = join(outputDirectory, "narrowslink.mjs"); const outputFiles = readdirSync(outputDirectory).sort(); @@ -43,7 +45,7 @@ try { const version = spawnSync(process.execPath, [symlinkPath, "version", "--json"], { encoding: "utf8" }); if (version.status !== 0) throw new Error(`CLI version exit was ${version.status}; expected 0.`); const identity = JSON.parse(version.stdout); - if (identity.name !== "narrowslink" || identity.version !== "0.1.0" || typeof identity.commit !== "string") { + if (identity.name !== "narrowslink" || identity.version !== expectedVersion || typeof identity.commit !== "string") { throw new Error("CLI release identity is not stable."); } } finally { diff --git a/scripts/narrowslink.ts b/scripts/narrowslink.ts index f778dca..f606a7f 100644 --- a/scripts/narrowslink.ts +++ b/scripts/narrowslink.ts @@ -29,7 +29,7 @@ declare const __NARROWSLINK_VERSION__: string; declare const __NARROWSLINK_COMMIT__: string; export const NARROWSLINK_RELEASE: ReleaseIdentity = Object.freeze({ - version: typeof __NARROWSLINK_VERSION__ === "string" ? __NARROWSLINK_VERSION__ : "0.1.0", + version: typeof __NARROWSLINK_VERSION__ === "string" ? __NARROWSLINK_VERSION__ : "0.2.0", commit: typeof __NARROWSLINK_COMMIT__ === "string" ? __NARROWSLINK_COMMIT__ : "unknown", }); diff --git a/src/capture/CaptureDialog.test.ts b/src/capture/CaptureDialog.test.ts index 1042668..ab7abac 100644 --- a/src/capture/CaptureDialog.test.ts +++ b/src/capture/CaptureDialog.test.ts @@ -92,6 +92,15 @@ describe("CaptureDialog UDP ownership", () => { }, "owned")).resolves.toBe(stopped); expect(stop).toHaveBeenCalledOnce(); }); + + it("rejects an owned stop response that remains active", async () => { + const stop = vi.fn(async () => bridgeStatus("owned", {}, "capturing")); + await expect(stopUdpCaptureIfOwned({ + getStatus: async () => bridgeStatus("owned"), + stop, + }, "owned")).rejects.toBeInstanceOf(UdpCaptureIntegrityError); + expect(stop).toHaveBeenCalledOnce(); + }); }); describe("CaptureDialog UDP integrity", () => { diff --git a/src/capture/CaptureDialog.tsx b/src/capture/CaptureDialog.tsx index 4a4e82d..a470d53 100644 --- a/src/capture/CaptureDialog.tsx +++ b/src/capture/CaptureDialog.tsx @@ -10,7 +10,15 @@ import { type RefObject, } from "react"; import { createPortal } from "react-dom"; -import { DownloadSimple, SpinnerGap, UploadSimple, WarningCircle, X } from "@phosphor-icons/react"; +import { + DownloadSimple, + FloppyDisk, + SpinnerGap, + Trash, + UploadSimple, + WarningCircle, + X, +} from "@phosphor-icons/react"; import type { CaptureIntegrityIssueCode, CaptureIntegrityReceipt, SessionDocument, UdpBridgeJournal } from "../domain/types"; import { @@ -29,6 +37,21 @@ import { MANUAL_OPERATOR_RUNTIME, type OperatorRuntime, } from "../runtime/operator-runtime"; +import { + clearCaptureProfiles, + createCaptureProfile, + loadCaptureProfiles, + removeCaptureProfile, + saveCaptureProfile, + type CaptureProfileDocument, + type CaptureProfileSettings, +} from "./capture-profile"; +import { + CapturePreflightAnalyzer, + MAX_PREFLIGHT_ANALYZED_BYTES, + MAX_PREFLIGHT_ANALYZED_RECORDS, + type CapturePreflightSummary, +} from "./capture-preflight"; import { createSerialAssembler, type SerialRecordAssembler, @@ -55,13 +78,24 @@ import { import { getBrowserSerialApi, WebSerialCapture, + type SerialCaptureHandlers, type SerialFlowControl, type SerialCaptureDevice, type SerialParity, } from "./web-serial"; type CaptureTransport = "udp" | "serial"; -type CapturePhase = "ready" | "starting" | "canceling" | "capturing" | "stopping" | "saving" | "save-error" | "finalize-error"; +type CapturePhase = + | "ready" + | "starting" + | "preflighting" + | "canceling" + | "capturing" + | "stopping" + | "saving" + | "save-error" + | "finalize-error"; +type CaptureStartingPurpose = "preflight" | "recording"; type SerialConnectionState = "idle" | "selecting" | "open" | "disconnected" | "closed" | "error"; interface CaptureTotals { @@ -369,7 +403,8 @@ export interface UdpCaptureIntegritySnapshot { /** * Refreshes status before issuing the global bridge stop command. The command * is never sent unless the currently reported capture ID is the one returned - * by this dialog's successful start request. + * by this dialog's successful start request, and success requires the bridge + * to confirm that capture is no longer active. */ export async function stopUdpCaptureIfOwned( client: UdpStopClient, @@ -388,6 +423,11 @@ export async function stopUdpCaptureIfOwned( "The bridge stop response identified a different capture. Clean save was refused.", ); } + if (stopped.state !== "stopped" && stopped.state !== "idle") { + throw new UdpCaptureIntegrityError( + `The bridge stop response remained in ${stopped.state} state. Capture shutdown was not confirmed.`, + ); + } return stopped; } @@ -547,8 +587,22 @@ function useDialogFocus( }, [dialogRef]); } -function capturePhaseLabel(phase: CapturePhase, transport: CaptureTransport, issue: string): string { - if (phase === "starting") return transport === "udp" ? "Connecting to bridge" : "Selecting serial device"; +function capturePhaseLabel( + phase: CapturePhase, + transport: CaptureTransport, + issue: string, + startingPurpose: CaptureStartingPurpose, + preflight: CapturePreflightSummary | null, +): string { + if (phase === "starting") { + if (startingPurpose === "recording") return "Establishing evidence boundary"; + return transport === "udp" ? "Opening UDP preflight" : "Selecting serial device"; + } + if (phase === "preflighting") { + if (preflight?.readiness === "ready") return "Preflight ready"; + if (preflight?.readiness === "attention") return "Preflight needs attention"; + return "Preflight waiting for traffic"; + } if (phase === "canceling") return "Capture setup cancelled"; if (phase === "capturing") return issue ? "Recording with attention required" : "Recording"; if (phase === "stopping") return "Stopping transport"; @@ -571,7 +625,9 @@ export function CaptureDialog({ const discardTriggerRef = useRef(null); const discardKeepRef = useRef(null); const decoderPackInputRef = useRef(null); + const profileNameInputRef = useRef(null); const transportRef = useRef(null); + const startingPurposeRef = useRef("preflight"); const recorderRef = useRef(null); const udpClientRef = useRef(null); const udpStatusRef = useRef(null); @@ -596,6 +652,9 @@ export function CaptureDialog({ const finalizationIssueCodesRef = useRef([]); const durationLimitEventRecordedRef = useRef(false); const serialCaptureRef = useRef(null); + const serialPreflightDeviceRef = useRef(null); + const serialPreflightSettingsRef = useRef["settings"] | null>(null); + const serialPreflightAssemblerRef = useRef(null); const serialProvenanceRef = useRef | null>(null); const serialAssemblerRef = useRef(null); const serialDisconnectedRef = useRef(false); @@ -613,6 +672,8 @@ export function CaptureDialog({ const startCancelledRef = useRef(false); const pendingFinalizedSessionRef = useRef(null); const pendingFinalizationRef = useRef(null); + const preflightAnalyzerRef = useRef(null); + const preflightConnectedRef = useRef(false); const mountedRef = useRef(true); const id = useId().replace(/:/g, ""); @@ -624,6 +685,7 @@ export function CaptureDialog({ const serialPanelId = `capture-serial-panel-${id}`; const udpTabId = `capture-udp-tab-${id}`; const serialTabId = `capture-serial-tab-${id}`; + const profileEditorId = `capture-profile-editor-${id}`; const [transport, setTransport] = useState("udp"); const [phase, setPhase] = useState("ready"); @@ -651,6 +713,13 @@ export function CaptureDialog({ const [issue, setIssue] = useState(""); const [notice, setNotice] = useState(""); const [confirmDiscard, setConfirmDiscard] = useState(false); + const [profiles, setProfiles] = useState([]); + const [selectedProfileId, setSelectedProfileId] = useState(""); + const [profileDirty, setProfileDirty] = useState(false); + const [profileEditorOpen, setProfileEditorOpen] = useState(false); + const [profileName, setProfileName] = useState(""); + const [profileStorageIssue, setProfileStorageIssue] = useState(""); + const [preflightSummary, setPreflightSummary] = useState(null); const [timerTick, setTimerTick] = useState(0); const captureLocked = phase !== "ready"; @@ -666,6 +735,9 @@ export function CaptureDialog({ const canDismiss = phase === "ready" || phase === "canceling"; const serialAvailable = useMemo(() => getBrowserSerialApi() != null, []); const elapsedUs = (() => { + if (phase === "preflighting" || (phase === "starting" && startingPurposeRef.current === "preflight")) { + return Math.floor((preflightSummary?.elapsedMs ?? 0) * 1_000); + } if (durationFrozenRef.current) return frozenDurationUsRef.current; if (captureStartMsRef.current === 0) return lastDurationUsRef.current; if (transportRef.current === "udp") { @@ -683,12 +755,33 @@ export function CaptureDialog({ })(); void timerTick; + useEffect(() => { + try { + setProfiles(loadCaptureProfiles()); + setProfileStorageIssue(""); + } catch (cause) { + setProfileStorageIssue(errorMessage(cause, "Saved capture profiles could not be loaded.")); + } + }, []); + + const markConfigurationChanged = (): void => { + setProfileDirty(selectedProfileId !== ""); + setPreflightSummary(null); + setIssue(""); + setNotice(""); + }; + + const markConnectionChanged = (): void => { + setPreflightSummary(null); + setIssue(""); + setNotice(""); + }; + const selectDecoderPack = (packHash: string): void => { const selected = decoderPacks.find((pack) => pack.integrity.canonicalSha256 === packHash); if (!selected || captureLocked) return; setDecoderPack(selected); - setIssue(""); - setNotice(""); + markConfigurationChanged(); }; const loadDecoderPack = async (event: ChangeEvent): Promise => { @@ -706,6 +799,8 @@ export function CaptureDialog({ const result = verifyDecoderPackConformance(input); setLocalDecoderPack(result.pack); setDecoderPack(result.pack); + setProfileDirty(selectedProfileId !== ""); + setPreflightSummary(null); setNotice( `Loaded ${result.pack.displayName} ${result.pack.revision}; ${result.fixtureIds.length} fixture${result.fixtureIds.length === 1 ? "" : "s"} passed.`, ); @@ -714,7 +809,137 @@ export function CaptureDialog({ } }; - const blockedClose = () => setNotice("Stop and save the capture, or explicitly discard it, before closing this dialog."); + const profileSettings = (): CaptureProfileSettings => { + if (transport === "udp") { + return { + transport: "udp", + host: udpHost.trim(), + port: strictInteger(udpPort, "UDP port", 0, 65_535), + multicastGroup: multicastGroup.trim() || null, + multicastInterface: multicastInterface.trim() || null, + }; + } + return { + transport: "serial", + baudRate: strictInteger(baudRate, "Baud rate", 1, 4_000_000), + dataBits: Number(dataBits) as 7 | 8, + stopBits: Number(stopBits) as 1 | 2, + parity, + flowControl, + }; + }; + + const applyProfile = (profileId: string): void => { + if (captureLocked) return; + if (!profileId) { + setSelectedProfileId(""); + setProfileDirty(false); + setProfileEditorOpen(false); + setIssue(""); + setNotice(""); + return; + } + const profile = profiles.find((candidate) => candidate.id === profileId); + if (!profile) return; + const pack = verifyDecoderPackConformance(profile.decoderPack).pack; + setDecoderPack(pack); + if (!BUILT_IN_DECODER_PACKS.some( + (candidate) => candidate.integrity.canonicalSha256 === pack.integrity.canonicalSha256, + )) { + setLocalDecoderPack(pack); + } + setTransport(profile.settings.transport); + if (profile.settings.transport === "udp") { + setUdpHost(profile.settings.host); + setUdpPort(String(profile.settings.port)); + setMulticastGroup(profile.settings.multicastGroup ?? ""); + setMulticastInterface(profile.settings.multicastInterface ?? ""); + } else { + setBaudRate(String(profile.settings.baudRate)); + setDataBits(String(profile.settings.dataBits) as "7" | "8"); + setStopBits(String(profile.settings.stopBits) as "1" | "2"); + setParity(profile.settings.parity); + setFlowControl(profile.settings.flowControl); + } + setSelectedProfileId(profile.id); + setProfileName(profile.name); + setProfileDirty(false); + setProfileEditorOpen(false); + setPreflightSummary(null); + setIssue(""); + setNotice(`Loaded ${profile.name}. Bridge credentials and session naming remain operator-entered.`); + }; + + const openProfileEditor = (): void => { + const selected = profiles.find((profile) => profile.id === selectedProfileId); + setProfileName( + selected?.name + ?? `${transport === "udp" ? "UDP" : "Serial"} · ${decoderPack.displayName}`, + ); + setProfileEditorOpen(true); + requestAnimationFrame(() => profileNameInputRef.current?.focus({ preventScroll: true })); + }; + + const persistProfile = (): void => { + if (captureLocked) return; + try { + const existing = profiles.find((profile) => profile.id === selectedProfileId); + const now = new Date().toISOString(); + const profile = createCaptureProfile({ + ...(existing ? { id: existing.id, createdAt: existing.createdAt } : {}), + name: profileName, + updatedAt: now, + decoderPack, + settings: profileSettings(), + }); + const next = saveCaptureProfile(profile); + setProfiles(next); + setSelectedProfileId(profile.id); + setProfileName(profile.name); + setProfileDirty(false); + setProfileEditorOpen(false); + setProfileStorageIssue(""); + setIssue(""); + setNotice(`${profile.name} was saved locally without bridge credentials, device permissions, or telemetry payloads.`); + } catch (cause) { + setProfileStorageIssue(errorMessage(cause, "The capture profile could not be saved.")); + } + }; + + const deleteSelectedProfile = (): void => { + if (captureLocked || !selectedProfileId) return; + const selected = profiles.find((profile) => profile.id === selectedProfileId); + try { + setProfiles(removeCaptureProfile(selectedProfileId)); + setSelectedProfileId(""); + setProfileDirty(false); + setProfileEditorOpen(false); + setProfileStorageIssue(""); + setNotice(`${selected?.name ?? "The capture profile"} was removed. Current field values were kept.`); + } catch (cause) { + setProfileStorageIssue(errorMessage(cause, "The capture profile could not be removed.")); + } + }; + + const resetProfileStorage = (): void => { + try { + clearCaptureProfiles(); + setProfiles([]); + setSelectedProfileId(""); + setProfileDirty(false); + setProfileEditorOpen(false); + setProfileStorageIssue(""); + setNotice("Saved capture profiles were reset. Current capture settings were not changed."); + } catch (cause) { + setProfileStorageIssue(errorMessage(cause, "Saved capture profiles could not be reset.")); + } + }; + + const blockedClose = () => setNotice( + phase === "preflighting" + ? "Stop the preflight source before closing this dialog." + : "Stop and save the capture, or explicitly discard it, before closing this dialog.", + ); useDialogFocus(dialogRef, onClose, canDismiss, blockedClose); useEffect(() => { @@ -728,8 +953,12 @@ export function CaptureDialog({ }, [phase]); useEffect(() => { - if (phase !== "starting" && phase !== "capturing") return; + if (phase !== "starting" && phase !== "preflighting" && phase !== "capturing") return; const flush = () => { + const preflightAnalyzer = preflightAnalyzerRef.current; + if (preflightAnalyzer && (phase === "preflighting" || startingPurposeRef.current === "preflight")) { + setPreflightSummary(preflightAnalyzer.snapshot(nowMonotonicMs(), preflightConnectedRef.current)); + } const activeTransport = transportRef.current; const recorder = recorderRef.current; setTotals({ @@ -749,7 +978,7 @@ export function CaptureDialog({ if (phase !== "starting") return; const timeout = globalThis.setTimeout(() => { startCancelledRef.current = true; - setNotice("Capture setup timed out. Any late transport open will be closed without recording; dismiss the device chooser if it remains visible."); + setNotice("Source setup timed out. Any late transport open will be closed without recording; dismiss the device chooser if it remains visible."); udpClientRef.current?.disconnect(); setPhase("canceling"); }, 20_000); @@ -829,6 +1058,15 @@ export function CaptureDialog({ udpStatusRef.current = null; }; + const resetForPreflight = (nextTransport: CaptureTransport): void => { + resetForStart(nextTransport); + const startedAtMs = nowMonotonicMs(); + startingPurposeRef.current = "preflight"; + preflightConnectedRef.current = false; + preflightAnalyzerRef.current = new CapturePreflightAnalyzer(decoderPack, startedAtMs); + setPreflightSummary(preflightAnalyzerRef.current.snapshot(startedAtMs, false)); + }; + const clearRuntime = (): void => { recorderRef.current = null; udpClientRef.current = null; @@ -853,6 +1091,9 @@ export function CaptureDialog({ observedUdpDatagramsRef.current = 0; observedUdpBytesRef.current = 0; serialCaptureRef.current = null; + serialPreflightDeviceRef.current = null; + serialPreflightSettingsRef.current = null; + serialPreflightAssemblerRef.current = null; serialAssemblerRef.current = null; serialDisconnectedRef.current = false; transportRef.current = null; @@ -862,8 +1103,11 @@ export function CaptureDialog({ captureInputClosedRef.current = false; startCancelledRef.current = false; captureStartMsRef.current = 0; + lastDurationUsRef.current = 0; lastRetainedOffsetUsRef.current = -1; verifiedStopDurationUsRef.current = 0; + preflightConnectedRef.current = false; + preflightAnalyzerRef.current = null; }; const captureEventOffsetUs = (): number => { @@ -1082,13 +1326,145 @@ export function CaptureDialog({ } }; - const startUdpCapture = async (): Promise => { + const acceptPreflightUdpDatagram = (datagram: UdpBridgeDatagram): void => { + const ownedCaptureId = ownedUdpCaptureIdRef.current; + if (!ownedCaptureId || datagram.captureId !== ownedCaptureId) return; + const analyzer = preflightAnalyzerRef.current; + if (!analyzer) return; + const observedAtMs = nowMonotonicMs(); + analyzer.observeInput(datagram.byteLength, observedAtMs); + analyzer.observeRecord({ + bytes: datagram.data, + offsetUs: datagram.offsetUs, + transport: "udp", + remoteEndpoint: { + address: datagram.remoteAddress, + port: datagram.remotePort, + family: datagram.remoteFamily, + }, + }); + lastDurationUsRef.current = Math.max(lastDurationUsRef.current, datagram.offsetUs + 1); + }; + + const startUdpPreflight = async (): Promise => { if (captureLocked || transportRef.current) return; if (operatorRuntime.mode === "invalid") { setIssue(operatorRuntime.message); return; } let port: number; + try { + port = strictInteger(udpPort, "UDP port", 0, 65_535); + } catch (cause) { + setIssue(errorMessage(cause, "The UDP preflight configuration is invalid.")); + return; + } + + resetForPreflight("udp"); + setPhase("starting"); + let client: UdpBridgeClient; + try { + client = new UdpBridgeClient({ + baseUrl: bridgeUrl.trim(), + ...(operatorRuntime.mode === "managed" + ? { authentication: { mode: "same-origin-proxy" as const } } + : { token: bridgeToken }), + onStatus: (status) => { + udpStatusRef.current = status; + if (mountedRef.current) setUdpStatus(status); + const ownedCaptureId = ownedUdpCaptureIdRef.current; + if (ownedCaptureId && status.capture?.id === ownedCaptureId) { + lastDurationUsRef.current = Math.max(lastDurationUsRef.current, status.capture.durationUs); + } + if (status.lastError && mountedRef.current) setIssue(udpErrorMessage(status.lastError)); + }, + onDatagram: (datagram) => { + if (transportRef.current !== "udp") return; + if (ownedUdpCaptureIdRef.current) { + acceptPreflightUdpDatagram(datagram); + } else if ( + pendingUdpDatagramsRef.current.length < MAX_PREFLIGHT_ANALYZED_RECORDS + && pendingUdpBytesRef.current + datagram.byteLength <= MAX_PREFLIGHT_ANALYZED_BYTES + ) { + pendingUdpDatagramsRef.current.push(datagram); + pendingUdpBytesRef.current += datagram.byteLength; + } else if (mountedRef.current) { + setIssue("UDP traffic exceeded the bounded preflight sample before ownership was confirmed. Stop and retry the preflight."); + } + }, + onError: (error) => { + preflightConnectedRef.current = false; + if (mountedRef.current) setIssue(udpErrorMessage(error)); + }, + }); + } catch (cause) { + clearRuntime(); + setPhase("ready"); + setIssue(errorMessage(cause, "The local UDP bridge configuration is invalid.")); + return; + } + udpClientRef.current = client; + + try { + const group = multicastGroup.trim(); + const interfaceAddress = multicastInterface.trim(); + const status = await client.start({ + host: udpHost.trim(), + port, + ...(group ? { multicastGroup: group } : {}), + ...(interfaceAddress ? { multicastInterface: interfaceAddress } : {}), + }); + if (startCancelledRef.current || !mountedRef.current) { + if (status.state === "capturing" && status.capture) { + ownedUdpCaptureIdRef.current = status.capture.id; + await stopUdpCaptureIfOwned(client, status.capture.id).catch(() => undefined); + } + client.disconnect(); + clearRuntime(); + if (mountedRef.current) { + setPhase("ready"); + setNotice("UDP preflight was cancelled before the source opened."); + } + return; + } + if (status.state !== "capturing" || !status.capture || !status.udp) { + throw new UdpCaptureOwnershipError( + "The bridge did not identify the UDP preflight capture and bind address.", + ); + } + ownedUdpCaptureIdRef.current = status.capture.id; + preflightConnectedRef.current = true; + captureStartMsRef.current = nowMonotonicMs() - status.capture.durationUs / 1_000; + udpStatusRef.current = status; + setUdpStatus(status); + const pending = pendingUdpDatagramsRef.current; + pendingUdpDatagramsRef.current = []; + pendingUdpBytesRef.current = 0; + for (const datagram of pending) acceptPreflightUdpDatagram(datagram); + setPhase("preflighting"); + } catch (cause) { + const cancelled = startCancelledRef.current; + const ownedCaptureId = ownedUdpCaptureIdRef.current; + if (ownedCaptureId) { + await stopUdpCaptureIfOwned(client, ownedCaptureId).catch(() => undefined); + } + client.disconnect(); + clearRuntime(); + if (mountedRef.current) { + setPhase("ready"); + if (cancelled) setNotice("UDP preflight was cancelled before the source opened."); + else setIssue(errorMessage(cause, "The UDP preflight could not be started.")); + } + } + }; + + const startUdpCapture = async (): Promise => { + if (transportRef.current) return; + if (operatorRuntime.mode === "invalid") { + setIssue(operatorRuntime.message); + return; + } + let port: number; let resolvedTimeZone: string; const title = sessionTitle.trim(); try { @@ -1101,6 +1477,7 @@ export function CaptureDialog({ } resetForStart("udp"); + startingPurposeRef.current = "recording"; udpRecorderConfigRef.current = { sessionId: createSessionId(), title, @@ -1211,7 +1588,54 @@ export function CaptureDialog({ } }; - const startSerialCapture = (): void => { + const createSerialRecordingHandlers = (): SerialCaptureHandlers => ({ + onChunk: (bytes) => { + if (transportRef.current !== "serial") return; + const offsetUs = Math.max(0, Math.floor((nowMonotonicMs() - captureStartMsRef.current) * 1_000)); + if (!durationFrozenRef.current) lastDurationUsRef.current = Math.max(lastDurationUsRef.current, offsetUs + 1); + observedSerialReadsRef.current += 1; + observedSerialBytesRef.current += bytes.byteLength; + if (captureInputClosedRef.current || ingestPausedRef.current) return; + try { + const assembler = serialAssemblerRef.current; + if (!assembler) throw new Error("The serial frame assembler was not initialized before input arrived."); + for (const assembly of assembler.push(bytes, offsetUs)) { + appendCapturedBytes({ offsetUs: assembly.offsetUs, bytes: assembly.bytes }); + } + } catch (cause) { + pauseIngest(cause); + } + }, + onError: (error) => { + freezeCaptureDuration(); + captureInputClosedRef.current = true; + transportIntegrityIssueRef.current = error.message; + recordTransportEvent(serialTransportFailureEvent( + "serial-read-error", + captureEventOffsetUs(), + error.message, + error.name || "serial-read-error", + )); + setSerialState("error"); + setIssue(`${error.message} Stop and save the records retained before the serial error.`); + }, + onDisconnect: () => { + freezeCaptureDuration(); + captureInputClosedRef.current = true; + transportIntegrityIssueRef.current = "The serial stream disconnected before the operator stopped it."; + recordTransportEvent(serialTransportFailureEvent( + "serial-disconnected", + captureEventOffsetUs(), + "The serial stream disconnected before the operator stopped it.", + "serial-stream-ended", + )); + serialDisconnectedRef.current = true; + setSerialState("disconnected"); + setIssue("The serial stream disconnected. Stop and save the records retained before the disconnect."); + }, + }); + + const startSerialPreflight = (): void => { if (captureLocked || transportRef.current) return; const api = getBrowserSerialApi(); if (!api) { @@ -1220,20 +1644,15 @@ export function CaptureDialog({ } let parsedBaudRate: number; - let resolvedTimeZone: string; - const title = sessionTitle.trim(); - const serialSessionId = createSessionId(); const captureDecoderPack = decoderPack; try { - if (!title) throw new Error("Session title is required."); parsedBaudRate = strictInteger(baudRate, "Baud rate", 1, 4_000_000); - resolvedTimeZone = validateTimeZone(timeZone); } catch (cause) { - setIssue(errorMessage(cause, "The serial capture configuration is invalid.")); + setIssue(errorMessage(cause, "The serial preflight configuration is invalid.")); return; } - resetForStart("serial"); + resetForPreflight("serial"); const normalizedSerialSettings: Extract["settings"] = { baudRate: parsedBaudRate, dataBits: Number(dataBits) as 7 | 8, @@ -1244,87 +1663,59 @@ export function CaptureDialog({ }; const serialCapture = new WebSerialCapture(api); serialCaptureRef.current = serialCapture; + serialPreflightSettingsRef.current = normalizedSerialSettings; + serialPreflightAssemblerRef.current = createSerialAssembler(captureDecoderPack); serialDisconnectedRef.current = false; setSerialDevice("Waiting for operator selection"); setSerialState("selecting"); setPhase("starting"); - // Keep this call in the Start button's synchronous event path. WebSerialCapture + // Keep this call in the Preflight button's synchronous event path. WebSerialCapture // invokes navigator.serial.requestPort() before its first await so the browser's // transient user activation is retained for the device chooser. const startPromise = serialCapture.start(normalizedSerialSettings, { onOpen: (device) => { if (startCancelledRef.current || !mountedRef.current) { - throw new Error("Serial setup was cancelled before recording began."); + throw new Error("Serial setup was cancelled before preflight began."); } - // WebSerialCapture invokes onOpen synchronously after port.open() and - // before readLoop(), making this the exact serial capture epoch. - serialProvenanceRef.current = serialCaptureProvenance(device, normalizedSerialSettings); - const recorder = new CaptureRecorder({ - sessionId: serialSessionId, - title, - startedAt: new Date(), - displayTimeZone: resolvedTimeZone, - source: { - id: `live-serial-${serialSessionId}`.slice(0, 128), - kind: "serial", - label: `${device.label} · ${parsedBaudRate.toLocaleString("en-US")} baud`.slice(0, 200), - }, - decoderPack: captureDecoderPack, - }); captureStartMsRef.current = nowMonotonicMs(); lastDurationUsRef.current = 0; - recorderRef.current = recorder; - flushPendingTransportEvents(recorder); - serialAssemblerRef.current = createSerialAssembler(captureDecoderPack); + serialPreflightDeviceRef.current = device; + preflightConnectedRef.current = true; setSerialDevice(device.label); setSerialState("open"); - setPhase("capturing"); + setPhase("preflighting"); }, onChunk: (bytes) => { if (transportRef.current !== "serial") return; const offsetUs = Math.max(0, Math.floor((nowMonotonicMs() - captureStartMsRef.current) * 1_000)); - if (!durationFrozenRef.current) lastDurationUsRef.current = Math.max(lastDurationUsRef.current, offsetUs + 1); - observedSerialReadsRef.current += 1; - observedSerialBytesRef.current += bytes.byteLength; - if (captureInputClosedRef.current) return; - if (ingestPausedRef.current) return; + const analyzer = preflightAnalyzerRef.current; + analyzer?.observeInput(bytes.byteLength, nowMonotonicMs()); try { - const assembler = serialAssemblerRef.current; - if (!assembler) throw new Error("The serial frame assembler was not initialized before input arrived."); + const assembler = serialPreflightAssemblerRef.current; + if (!assembler) throw new Error("The serial preflight assembler was not initialized before input arrived."); for (const assembly of assembler.push(bytes, offsetUs)) { - appendCapturedBytes({ offsetUs: assembly.offsetUs, bytes: assembly.bytes }); + analyzer?.observeRecord({ + bytes: assembly.bytes, + offsetUs: assembly.offsetUs, + transport: "serial", + }); } } catch (cause) { - pauseIngest(cause); + preflightConnectedRef.current = false; + setIssue(errorMessage(cause, "Serial preflight analysis failed.")); } }, onError: (error) => { - freezeCaptureDuration(); - captureInputClosedRef.current = true; - transportIntegrityIssueRef.current = error.message; - recordTransportEvent(serialTransportFailureEvent( - "serial-read-error", - captureEventOffsetUs(), - error.message, - error.name || "serial-read-error", - )); + preflightConnectedRef.current = false; setSerialState("error"); - setIssue(`${error.message} Stop and save the records retained before the serial error.`); + setIssue(`${error.message} Stop the preflight and check the device and serial settings.`); }, onDisconnect: () => { - freezeCaptureDuration(); - captureInputClosedRef.current = true; - transportIntegrityIssueRef.current = "The serial stream disconnected before the operator stopped it."; - recordTransportEvent(serialTransportFailureEvent( - "serial-disconnected", - captureEventOffsetUs(), - "The serial stream disconnected before the operator stopped it.", - "serial-stream-ended", - )); + preflightConnectedRef.current = false; serialDisconnectedRef.current = true; setSerialState("disconnected"); - setIssue("The serial stream disconnected. Stop and save the records retained before the disconnect."); + setIssue("The serial stream disconnected during preflight. Stop the preflight and check the device."); }, }); @@ -1338,10 +1729,134 @@ export function CaptureDialog({ if (mountedRef.current) { setSerialState("error"); setPhase("ready"); - if (cancelled) setNotice("Serial capture setup was cancelled before recording began."); - else setIssue(errorMessage(cause, "The serial capture could not be started.")); + if (cancelled) setNotice("Serial preflight was cancelled before the source opened."); + else setIssue(errorMessage(cause, "The serial preflight could not be started.")); + } + }); + }; + + const stopActivePreflight = async (): Promise => { + if (transportRef.current === "udp") { + const client = udpClientRef.current; + const ownedCaptureId = ownedUdpCaptureIdRef.current; + if (client && ownedCaptureId) await stopUdpCaptureIfOwned(client, ownedCaptureId); + client?.disconnect(); + return; + } + if (transportRef.current === "serial") { + await serialCaptureRef.current?.stop(); + } + }; + + const stopPreflight = async (): Promise => { + if (phase !== "preflighting" || finalizingRef.current) return; + finalizingRef.current = true; + setPhase("stopping"); + let stopWarning = ""; + try { + await stopActivePreflight(); + } catch (cause) { + stopWarning = ` The source did not confirm a clean preflight stop: ${errorMessage(cause, "unknown stop failure")}`; + udpClientRef.current?.disconnect(); + await serialCaptureRef.current?.stop().catch(() => undefined); + } + clearRuntime(); + setUdpStatus(null); + udpStatusRef.current = null; + setSerialState("closed"); + setPhase("ready"); + setIssue(""); + setNotice(`Preflight stopped. Its sampled bytes were not added to a session.${stopWarning}`); + finalizingRef.current = false; + }; + + const beginUdpRecording = async (): Promise => { + if (phase !== "preflighting" || transportRef.current !== "udp" || finalizingRef.current) return; + const title = sessionTitle.trim(); + try { + if (!title) throw new Error("Session title is required."); + validateTimeZone(timeZone); + } catch (cause) { + setIssue(errorMessage(cause, "Session metadata is invalid.")); + return; + } + + finalizingRef.current = true; + startingPurposeRef.current = "recording"; + setPhase("starting"); + try { + await stopActivePreflight(); + } catch (cause) { + udpClientRef.current?.disconnect(); + clearRuntime(); + setPhase("ready"); + setIssue(`${errorMessage(cause, "UDP preflight did not stop cleanly.")} Recording was not started because a new evidence boundary could not be established.`); + finalizingRef.current = false; + return; + } + clearRuntime(); + finalizingRef.current = false; + await startUdpCapture(); + }; + + const beginSerialRecording = (): void => { + if (phase !== "preflighting" || transportRef.current !== "serial") return; + const serialCapture = serialCaptureRef.current; + const device = serialPreflightDeviceRef.current; + const settings = serialPreflightSettingsRef.current; + let resolvedTimeZone: string; + const title = sessionTitle.trim(); + try { + if (!title) throw new Error("Session title is required."); + resolvedTimeZone = validateTimeZone(timeZone); + if (!serialCapture?.active || !device || !settings) { + throw new Error("The serial preflight source is no longer open."); } + } catch (cause) { + setIssue(errorMessage(cause, "Session metadata is invalid.")); + return; + } + + const sessionId = createSessionId(); + const captureDecoderPack = decoderPack; + resetForStart("serial"); + startingPurposeRef.current = "recording"; + serialProvenanceRef.current = serialCaptureProvenance(device, settings); + const recorder = new CaptureRecorder({ + sessionId, + title, + startedAt: new Date(), + displayTimeZone: resolvedTimeZone, + source: { + id: `live-serial-${sessionId}`.slice(0, 128), + kind: "serial", + label: `${device.label} · ${settings.baudRate.toLocaleString("en-US")} baud`.slice(0, 200), + }, + decoderPack: captureDecoderPack, }); + recorderRef.current = recorder; + serialAssemblerRef.current = createSerialAssembler(captureDecoderPack); + captureStartMsRef.current = nowMonotonicMs(); + lastDurationUsRef.current = 0; + preflightConnectedRef.current = false; + serialPreflightAssemblerRef.current = null; + try { + serialCapture.replaceHandlers(createSerialRecordingHandlers()); + } catch (cause) { + recorderRef.current = null; + serialAssemblerRef.current = null; + setPhase("preflighting"); + setIssue(errorMessage(cause, "The serial evidence boundary could not be established.")); + return; + } + flushPendingTransportEvents(recorder); + setSerialState("open"); + setPhase("capturing"); + }; + + const beginRecording = (): void => { + if (transport === "udp") void beginUdpRecording(); + else beginSerialRecording(); }; const stopTransport = async (): Promise => { @@ -1729,8 +2244,7 @@ export function CaptureDialog({ const chooseTransport = (nextTransport: CaptureTransport): void => { if (captureLocked) return; setTransport(nextTransport); - setIssue(""); - setNotice(""); + markConfigurationChanged(); }; const handleTabKey = (event: ReactKeyboardEvent): void => { @@ -1743,16 +2257,37 @@ export function CaptureDialog({ const formSubmit = (event: FormEvent): void => { event.preventDefault(); - if (transport === "udp") void startUdpCapture(); + if (phase !== "ready") return; + if (transport === "udp") void startUdpPreflight(); }; - const phaseLabel = capturePhaseLabel(phase, transport, issue); + const phaseLabel = capturePhaseLabel( + phase, + transport, + issue, + startingPurposeRef.current, + preflightSummary, + ); const unitLabel = transport === "udp" ? "Datagrams received" : "Serial reads received"; const transportDetail = transport === "udp" ? udpStatus?.udp ? `${udpStatus.udp.host}:${udpStatus.udp.port}${udpStatus.udp.family ? ` · ${udpStatus.udp.family}` : ""}` : phase === "starting" ? "Negotiating local bind" : "Not bound" : serialDevice; + const preflightFamilyLabel = preflightSummary?.families.length + ? preflightSummary.families.slice(0, 2).map((family) => `${family.name} (${family.count})`).join(", ") + : "None observed"; + const preflightEndpointLabel = preflightSummary?.endpoints.length + ? preflightSummary.endpoints + .slice(0, 2) + .map((endpoint) => `${endpoint.address}:${endpoint.port}`) + .join(", ") + : "None observed"; + const preflightActionLabel = preflightSummary?.readiness === "ready" + ? "Start recording" + : preflightSummary?.inputUnits + ? "Record with warning" + : "Start recording anyway"; return createPortal(
Capture exact UDP datagrams or assembled serial records locally, decode them with an identified pack, then open the immutable session in the replay workspace.

+
+ + + +

+ {selectedProfileId + ? `${profiles.find((profile) => profile.id === selectedProfileId)?.name ?? "Selected profile"}${profileDirty ? " · modified" : " · loaded"}` + : "Profiles store transport settings and the exact decoder pack locally. Credentials, device permission, and telemetry are excluded."} +

+ {profileEditorOpen && ( +
+ + + +
+ )} + {profileStorageIssue && ( +
+ {profileStorageIssue} + +
+ )} +
+
@@ -1941,7 +2563,10 @@ export function CaptureDialog({ maxLength={253} value={udpHost} disabled={captureLocked} - onChange={(event) => setUdpHost(event.target.value)} + onChange={(event) => { + setUdpHost(event.target.value); + markConfigurationChanged(); + }} spellCheck={false} /> @@ -1956,7 +2581,10 @@ export function CaptureDialog({ inputMode="numeric" value={udpPort} disabled={captureLocked} - onChange={(event) => setUdpPort(event.target.value)} + onChange={(event) => { + setUdpPort(event.target.value); + markConfigurationChanged(); + }} /> Use 0 to let the bridge choose an available local port. @@ -1966,7 +2594,10 @@ export function CaptureDialog({ maxLength={253} value={multicastGroup} disabled={captureLocked} - onChange={(event) => setMulticastGroup(event.target.value)} + onChange={(event) => { + setMulticastGroup(event.target.value); + markConfigurationChanged(); + }} placeholder="239.255.42.99" spellCheck={false} /> @@ -1977,7 +2608,10 @@ export function CaptureDialog({ maxLength={253} value={multicastInterface} disabled={captureLocked} - onChange={(event) => setMulticastInterface(event.target.value)} + onChange={(event) => { + setMulticastInterface(event.target.value); + markConfigurationChanged(); + }} placeholder="127.0.0.1" spellCheck={false} /> @@ -2005,26 +2639,38 @@ export function CaptureDialog({ inputMode="numeric" value={baudRate} disabled={captureLocked} - onChange={(event) => setBaudRate(event.target.value)} + onChange={(event) => { + setBaudRate(event.target.value); + markConfigurationChanged(); + }} />
-
-
Source
{transportDetail}
-
Elapsed
{formatDurationUs(elapsedUs, true)}
-
{unitLabel}
{totals.inputUnits.toLocaleString()}
-
Input bytes
{formatBytes(totals.inputBytes)}
-
Records retained
{totals.records.toLocaleString()}
-
Bytes retained
{formatBytes(totals.recordedBytes)}
-
+ {phase === "preflighting" || (phase === "starting" && startingPurposeRef.current === "preflight") ? ( +
+
Source
{transportDetail}
+
+
Traffic
+
{(preflightSummary?.unitRate ?? 0).toFixed(1)}/s · {formatBytes(Math.round(preflightSummary?.byteRate ?? 0))}/s
+
+
+
Last input
+
+ {preflightSummary?.lastInputAgeMs == null + ? "Not observed" + : preflightSummary.lastInputAgeMs < 1_000 + ? "Now" + : `${(preflightSummary.lastInputAgeMs / 1_000).toFixed(1)}s ago`} +
+
+
Valid frames
{(preflightSummary?.completeFrames ?? 0).toLocaleString()}
+
+
Malformed
+
+ {(preflightSummary?.malformedFrames ?? 0).toLocaleString()} + {(preflightSummary?.checksumFailures ?? 0) > 0 ? ` · ${preflightSummary?.checksumFailures} checksum` : ""} +
+
+
Message families
{preflightFamilyLabel}
+
+ ) : ( +
+
Source
{transportDetail}
+
Elapsed
{formatDurationUs(elapsedUs, true)}
+
{unitLabel}
{totals.inputUnits.toLocaleString()}
+
Input bytes
{formatBytes(totals.inputBytes)}
+
Records retained
{totals.records.toLocaleString()}
+
Bytes retained
{formatBytes(totals.recordedBytes)}
+
+ )}

Decoder: {decoderPack.displayName} · {decoderPack.revision} · {decoderPack.integrity.canonicalSha256.slice(0, 12)}

+ {phase === "preflighting" && preflightSummary && ( + <> +

+ {preflightSummary.message} +

+

+ Sampled: {preflightSummary.inputUnits.toLocaleString()} {transport === "udp" ? "datagrams" : "serial reads"} · {formatBytes(preflightSummary.inputBytes)} + {transport === "udp" ? ` · endpoints ${preflightEndpointLabel}` : ""} + {preflightSummary.analysisLimited ? " · analysis limit reached" : ""} +

+

+ Preflight samples are not retained. {transport === "udp" + ? "Starting recording stops this probe and opens a new owned capture ID." + : "Starting recording resets framing and routes only future serial reads into the session."} +

+ + )} {transport === "udp" && udpStatus && (

Bridge state: {udpStatus.state} · subscribers: {udpStatus.subscribers.toLocaleString()} @@ -2068,7 +2763,13 @@ export function CaptureDialog({ {transport === "serial" && (

Serial state: {serialState}

)} - {!canDismiss &&

Closing is locked until the source is stopped and the capture is saved or explicitly discarded.

} + {!canDismiss && ( +

+ {phase === "preflighting" + ? "Closing is locked while the preflight source is open." + : "Closing is locked until the source is stopped and the capture is saved or explicitly discarded."} +

+ )} {issue && } @@ -2093,20 +2794,35 @@ export function CaptureDialog({ disabled={operatorRuntime.mode === "invalid"} data-capture-phase-focus="ready" > - Start UDP capture + Run UDP preflight ) : ( )} + ) : phase === "preflighting" ? ( + <> + + + ) : phase === "starting" ? ( ) : phase === "canceling" ? ( diff --git a/src/capture/capture-preflight.test.ts b/src/capture/capture-preflight.test.ts new file mode 100644 index 0000000..4d8d67c --- /dev/null +++ b/src/capture/capture-preflight.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from "vitest"; + +import { + NMEA0183_DECODER_PACK, + NSL01_DECODER_PACK, + encodeFrame, + hexToBytes, +} from "../domain/decoder"; +import { + CapturePreflightAnalyzer, + MAX_PREFLIGHT_ANALYZED_RECORDS, +} from "./capture-preflight"; + +function heartbeatFrame(): Uint8Array { + const payload = new Uint8Array(8); + const view = new DataView(payload.buffer); + view.setUint32(0, 20, true); + payload[4] = 2; + view.setUint16(6, 0x0137, true); + return encodeFrame({ + familyId: 0x02, + sequence: 1, + deviceTimeMs: 20, + payload, + }); +} + +describe("capture preflight analysis", () => { + it("confirms transport and decoder fit without retaining payload bytes", () => { + const analyzer = new CapturePreflightAnalyzer(NSL01_DECODER_PACK, 1_000); + const bytes = heartbeatFrame(); + analyzer.observeInput(bytes.byteLength, 1_100); + analyzer.observeRecord({ + bytes, + offsetUs: 100_000, + transport: "udp", + remoteEndpoint: { address: "127.0.0.1", port: 9_104, family: "IPv4" }, + }); + + expect(analyzer.snapshot(2_000, true)).toMatchObject({ + connected: true, + inputUnits: 1, + inputBytes: bytes.byteLength, + completeFrames: 1, + malformedFrames: 0, + readiness: "ready", + decoderFit: "confirmed", + families: [{ name: "Heartbeat", count: 1 }], + endpoints: [{ address: "127.0.0.1", port: 9_104, family: "IPv4" }], + }); + expect(Object.keys(analyzer.snapshot(2_000, true))).not.toContain("records"); + }); + + it("distinguishes no traffic from traffic that does not fit the decoder", () => { + const waiting = new CapturePreflightAnalyzer(NMEA0183_DECODER_PACK, 0); + expect(waiting.snapshot(10_001, true)).toMatchObject({ + readiness: "waiting", + decoderFit: "not-observed", + message: expect.stringContaining("No traffic observed"), + }); + + const mismatch = new CapturePreflightAnalyzer(NMEA0183_DECODER_PACK, 0); + const noise = new TextEncoder().encode("not nmea\r\n"); + mismatch.observeInput(noise.byteLength, 100); + mismatch.observeRecord({ bytes: noise, offsetUs: 100_000, transport: "serial" }); + expect(mismatch.snapshot(1_000, true)).toMatchObject({ + readiness: "attention", + decoderFit: "not-confirmed", + completeFrames: 0, + malformedFrames: 1, + }); + }); + + it("reports checksum failures and valid NMEA sentence families", () => { + const analyzer = new CapturePreflightAnalyzer(NMEA0183_DECODER_PACK, 0); + const valid = hexToBytes(NMEA0183_DECODER_PACK.fixtures[0]!.records[0]!.dataHex); + const invalid = hexToBytes(NMEA0183_DECODER_PACK.fixtures[2]!.records[0]!.dataHex); + for (const [index, bytes] of [valid, invalid].entries()) { + analyzer.observeInput(bytes.byteLength, index * 100); + analyzer.observeRecord({ bytes, offsetUs: index * 100_000, transport: "udp" }); + } + expect(analyzer.snapshot(1_000, true)).toMatchObject({ + completeFrames: 1, + malformedFrames: 1, + checksumFailures: 1, + decoderFit: "confirmed", + }); + }); + + it("bounds analysis while continuing to count observed input", () => { + const analyzer = new CapturePreflightAnalyzer(NSL01_DECODER_PACK, 0); + const bytes = heartbeatFrame(); + for (let index = 0; index < MAX_PREFLIGHT_ANALYZED_RECORDS + 2; index += 1) { + analyzer.observeInput(bytes.byteLength, index); + analyzer.observeRecord({ bytes, offsetUs: index, transport: "udp" }); + } + expect(analyzer.snapshot(1_000, true)).toMatchObject({ + inputUnits: MAX_PREFLIGHT_ANALYZED_RECORDS + 2, + analyzedRecords: MAX_PREFLIGHT_ANALYZED_RECORDS, + analysisLimited: true, + }); + }); +}); diff --git a/src/capture/capture-preflight.ts b/src/capture/capture-preflight.ts new file mode 100644 index 0000000..2af3796 --- /dev/null +++ b/src/capture/capture-preflight.ts @@ -0,0 +1,191 @@ +import { bytesToHex, decodeRecord } from "../domain/decoder"; +import type { DecoderPackDocument } from "../domain/decoder-pack"; +import type { SourceRecord, UdpRemoteEndpoint } from "../domain/types"; + +export const MAX_PREFLIGHT_ANALYZED_RECORDS = 256; +export const MAX_PREFLIGHT_ANALYZED_BYTES = 512 * 1024; +export const MAX_PREFLIGHT_ENDPOINTS = 16; + +export type PreflightReadiness = "waiting" | "ready" | "attention"; +export type PreflightDecoderFit = "not-observed" | "confirmed" | "not-confirmed"; + +export interface CapturePreflightSummary { + readonly connected: boolean; + readonly elapsedMs: number; + readonly inputUnits: number; + readonly inputBytes: number; + readonly unitRate: number; + readonly byteRate: number; + readonly lastInputAgeMs: number | null; + readonly analyzedRecords: number; + readonly analyzedBytes: number; + readonly completeFrames: number; + readonly malformedFrames: number; + readonly checksumFailures: number; + readonly families: readonly { name: string; count: number }[]; + readonly endpoints: readonly UdpRemoteEndpoint[]; + readonly omittedEndpoints: number; + readonly analysisLimited: boolean; + readonly readiness: PreflightReadiness; + readonly decoderFit: PreflightDecoderFit; + readonly message: string; +} + +export interface PreflightRecordInput { + readonly bytes: Uint8Array; + readonly offsetUs: number; + readonly transport: "udp" | "serial"; + readonly remoteEndpoint?: UdpRemoteEndpoint; +} + +function endpointKey(endpoint: UdpRemoteEndpoint): string { + return `${endpoint.family}\u0000${endpoint.address}\u0000${endpoint.port}`; +} + +function boundedElapsed(startedAtMs: number, nowMs: number): number { + if (!Number.isFinite(nowMs)) return 0; + return Math.max(0, nowMs - startedAtMs); +} + +export class CapturePreflightAnalyzer { + private inputUnits = 0; + private inputBytes = 0; + private lastInputAtMs: number | null = null; + private analyzedRecords = 0; + private analyzedBytes = 0; + private completeFrames = 0; + private malformedFrames = 0; + private checksumFailures = 0; + private analysisLimited = false; + private readonly familyCounts = new Map(); + private readonly endpoints = new Map(); + private omittedEndpoints = 0; + + constructor( + private readonly decoderPack: DecoderPackDocument, + private readonly startedAtMs: number, + ) {} + + observeInput(byteLength: number, observedAtMs: number): void { + if (!Number.isSafeInteger(byteLength) || byteLength < 0) { + throw new Error("Preflight input byte lengths must be non-negative integers."); + } + this.inputUnits += 1; + this.inputBytes += byteLength; + this.lastInputAtMs = Math.max(this.lastInputAtMs ?? observedAtMs, observedAtMs); + } + + observeRecord(input: PreflightRecordInput): void { + if (!(input.bytes instanceof Uint8Array)) { + throw new Error("Preflight records must contain Uint8Array bytes."); + } + if (!Number.isSafeInteger(input.offsetUs) || input.offsetUs < 0) { + throw new Error("Preflight record offsets must be non-negative integer microseconds."); + } + if ( + this.analyzedRecords >= MAX_PREFLIGHT_ANALYZED_RECORDS + || this.analyzedBytes + input.bytes.byteLength > MAX_PREFLIGHT_ANALYZED_BYTES + ) { + this.analysisLimited = true; + return; + } + + const record: SourceRecord = { + id: `preflight-${this.analyzedRecords}`, + index: this.analyzedRecords, + sourceId: "preflight", + offsetUs: input.offsetUs, + dataHex: bytesToHex(input.bytes), + captureBytes: input.bytes.byteLength, + wireBytes: input.bytes.byteLength, + transport: { + kind: input.transport, + ...(input.remoteEndpoint ? { remoteEndpoint: input.remoteEndpoint } : {}), + }, + }; + const frame = decodeRecord(record, this.analyzedRecords, this.decoderPack); + this.analyzedRecords += 1; + this.analyzedBytes += input.bytes.byteLength; + if (frame.status === "complete" && frame.integrity.status === "valid") { + this.completeFrames += 1; + } else { + this.malformedFrames += 1; + if (frame.integrity.status === "crc-failed" || frame.integrity.status === "checksum-failed") { + this.checksumFailures += 1; + } + } + this.familyCounts.set(frame.familyName, (this.familyCounts.get(frame.familyName) ?? 0) + 1); + + if (input.remoteEndpoint) { + const key = endpointKey(input.remoteEndpoint); + if (!this.endpoints.has(key)) { + if (this.endpoints.size < MAX_PREFLIGHT_ENDPOINTS) { + this.endpoints.set(key, { ...input.remoteEndpoint }); + } else { + this.omittedEndpoints += 1; + } + } + } + } + + snapshot(nowMs: number, connected: boolean): CapturePreflightSummary { + const elapsedMs = boundedElapsed(this.startedAtMs, nowMs); + const elapsedSeconds = Math.max(elapsedMs / 1_000, 0.001); + const lastInputAgeMs = this.lastInputAtMs == null + ? null + : Math.max(0, nowMs - this.lastInputAtMs); + const families = [...this.familyCounts.entries()] + .map(([name, count]) => ({ name, count })) + .sort((left, right) => right.count - left.count || left.name.localeCompare(right.name, "en")); + + let readiness: PreflightReadiness; + let decoderFit: PreflightDecoderFit; + let message: string; + if (!connected) { + readiness = "waiting"; + decoderFit = "not-observed"; + message = "Opening the local source."; + } else if (this.inputUnits === 0) { + readiness = "waiting"; + decoderFit = "not-observed"; + message = elapsedMs >= 10_000 + ? "No traffic observed. Check the sender, bind or multicast interface, cable, and radio link." + : "Source is open; waiting for traffic."; + } else if (this.completeFrames === 0) { + readiness = "attention"; + decoderFit = "not-confirmed"; + message = this.analyzedRecords === 0 + ? "Traffic is arriving, but no complete record boundary has been observed." + : "Traffic is arriving, but the selected decoder has not produced a valid frame. Check decoder, framing, and serial settings."; + } else { + decoderFit = "confirmed"; + const malformedRatio = this.malformedFrames / Math.max(1, this.analyzedRecords); + readiness = malformedRatio > 0.5 ? "attention" : "ready"; + message = readiness === "ready" + ? "Traffic and decoder fit are confirmed." + : "Valid frames are present, but most observed records are malformed. Recording remains available with an explicit warning."; + } + + return { + connected, + elapsedMs, + inputUnits: this.inputUnits, + inputBytes: this.inputBytes, + unitRate: this.inputUnits / elapsedSeconds, + byteRate: this.inputBytes / elapsedSeconds, + lastInputAgeMs, + analyzedRecords: this.analyzedRecords, + analyzedBytes: this.analyzedBytes, + completeFrames: this.completeFrames, + malformedFrames: this.malformedFrames, + checksumFailures: this.checksumFailures, + families, + endpoints: [...this.endpoints.values()], + omittedEndpoints: this.omittedEndpoints, + analysisLimited: this.analysisLimited, + readiness, + decoderFit, + message, + }; + } +} diff --git a/src/capture/capture-profile.test.ts b/src/capture/capture-profile.test.ts new file mode 100644 index 0000000..84568fd --- /dev/null +++ b/src/capture/capture-profile.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, it } from "vitest"; + +import { NMEA0183_DECODER_PACK, NSL01_DECODER_PACK } from "../domain/decoder"; +import { + CAPTURE_PROFILE_STORAGE_KEY, + CaptureProfileStorageError, + clearCaptureProfiles, + createCaptureProfile, + loadCaptureProfiles, + removeCaptureProfile, + saveCaptureProfile, + type CaptureProfileStorageLike, +} from "./capture-profile"; + +function memoryStorage(): CaptureProfileStorageLike & { values: Map } { + const values = new Map(); + return { + values, + getItem: (key) => values.get(key) ?? null, + setItem: (key, value) => { + values.set(key, value); + }, + removeItem: (key) => { + values.delete(key); + }, + }; +} + +describe("capture profiles", () => { + it("round-trips an exact decoder pack and transport settings without secrets or payloads", () => { + const storage = memoryStorage(); + const profile = createCaptureProfile({ + id: "profile-harbor-multicast", + name: "Harbor multicast", + decoderPack: NMEA0183_DECODER_PACK, + settings: { + transport: "udp", + host: "0.0.0.0", + port: 10_110, + multicastGroup: "239.255.42.99", + multicastInterface: "192.168.4.20", + }, + createdAt: "2026-07-25T12:00:00.000Z", + updatedAt: "2026-07-25T12:00:00.000Z", + }); + + saveCaptureProfile(profile, storage); + expect(loadCaptureProfiles(storage)).toEqual([profile]); + const serialized = storage.values.get(CAPTURE_PROFILE_STORAGE_KEY) ?? ""; + expect(serialized).toContain(NMEA0183_DECODER_PACK.integrity.canonicalSha256); + expect(serialized).not.toContain("bridgeToken"); + expect(serialized).not.toContain("sessionTitle"); + expect(serialized).not.toContain("payload"); + }); + + it("updates by stable profile id, orders by recency, and removes explicitly", () => { + const storage = memoryStorage(); + const first = createCaptureProfile({ + id: "profile-radio", + name: "Surface radio", + decoderPack: NSL01_DECODER_PACK, + settings: { + transport: "serial", + baudRate: 57_600, + dataBits: 8, + stopBits: 1, + parity: "none", + flowControl: "none", + }, + createdAt: "2026-07-25T12:00:00.000Z", + updatedAt: "2026-07-25T12:00:00.000Z", + }); + const second = createCaptureProfile({ + id: "profile-udp", + name: "Bench UDP", + decoderPack: NSL01_DECODER_PACK, + settings: { + transport: "udp", + host: "127.0.0.1", + port: 9_104, + multicastGroup: null, + multicastInterface: null, + }, + createdAt: "2026-07-25T12:01:00.000Z", + updatedAt: "2026-07-25T12:01:00.000Z", + }); + + saveCaptureProfile(first, storage); + expect(saveCaptureProfile(second, storage).map((profile) => profile.id)).toEqual([ + "profile-udp", + "profile-radio", + ]); + const updated = createCaptureProfile({ + ...first, + name: "Surface radio 57k", + updatedAt: "2026-07-25T12:02:00.000Z", + }); + expect(saveCaptureProfile(updated, storage).map((profile) => profile.name)).toEqual([ + "Surface radio 57k", + "Bench UDP", + ]); + expect(removeCaptureProfile("profile-radio", storage)).toEqual([second]); + clearCaptureProfiles(storage); + expect(loadCaptureProfiles(storage)).toEqual([]); + }); + + it("rejects altered decoder packs and corrupt stored collections", () => { + const storage = memoryStorage(); + const alteredPack = structuredClone(NSL01_DECODER_PACK); + alteredPack.displayName = "Altered"; + expect(() => createCaptureProfile({ + id: "profile-altered", + name: "Altered", + decoderPack: alteredPack, + settings: { + transport: "udp", + host: "127.0.0.1", + port: 9_104, + multicastGroup: null, + multicastInterface: null, + }, + })).toThrow(CaptureProfileStorageError); + + storage.values.set(CAPTURE_PROFILE_STORAGE_KEY, "{\"format\":\"wrong\"}"); + expect(() => loadCaptureProfiles(storage)).toThrow(CaptureProfileStorageError); + }); + + it("leaves capture usable when local profile storage is unavailable", () => { + expect(() => loadCaptureProfiles(null)).toThrowError( + /Capture remains usable without saved profiles/, + ); + }); +}); diff --git a/src/capture/capture-profile.ts b/src/capture/capture-profile.ts new file mode 100644 index 0000000..1b52f4f --- /dev/null +++ b/src/capture/capture-profile.ts @@ -0,0 +1,312 @@ +import { z } from "zod"; + +import { verifyDecoderPackConformance } from "../domain/decoder-conformance"; +import { + decoderPackDocumentSchema, + type DecoderPackDocument, +} from "../domain/decoder-pack"; + +export const CAPTURE_PROFILE_FORMAT = "narrowslink/capture-profile"; +export const CAPTURE_PROFILE_FORMAT_VERSION = 1; +export const CAPTURE_PROFILE_STORAGE_KEY = "narrowslink.capture-profiles.v1"; +export const MAX_CAPTURE_PROFILES = 16; +export const MAX_CAPTURE_PROFILE_STORAGE_BYTES = 2 * 1024 * 1024; + +const timestampSchema = z.string().max(64).refine( + (value) => Number.isFinite(Date.parse(value)), + "Capture profile timestamps must be valid ISO timestamps.", +); + +const udpSettingsSchema = z.object({ + transport: z.literal("udp"), + host: z.string().min(1).max(253), + port: z.number().int().min(0).max(65_535), + multicastGroup: z.string().min(1).max(253).nullable(), + multicastInterface: z.string().min(1).max(253).nullable(), +}).strict(); + +const serialSettingsSchema = z.object({ + transport: z.literal("serial"), + baudRate: z.number().int().min(1).max(4_000_000), + dataBits: z.union([z.literal(7), z.literal(8)]), + stopBits: z.union([z.literal(1), z.literal(2)]), + parity: z.enum(["none", "even", "odd"]), + flowControl: z.enum(["none", "hardware"]), +}).strict(); + +export const captureProfileDocumentSchema = z.object({ + format: z.literal(CAPTURE_PROFILE_FORMAT), + formatVersion: z.literal(CAPTURE_PROFILE_FORMAT_VERSION), + id: z.string().min(1).max(128), + name: z.string().trim().min(1).max(80), + createdAt: timestampSchema, + updatedAt: timestampSchema, + decoderPack: decoderPackDocumentSchema, + settings: z.discriminatedUnion("transport", [ + udpSettingsSchema, + serialSettingsSchema, + ]), +}).strict(); + +const captureProfileCollectionSchema = z.object({ + format: z.literal("narrowslink/capture-profiles"), + formatVersion: z.literal(1), + profiles: z.array(captureProfileDocumentSchema).max(MAX_CAPTURE_PROFILES), +}).strict(); + +export type CaptureProfileDocument = z.infer; +export type CaptureProfileSettings = CaptureProfileDocument["settings"]; + +export interface CaptureProfileStorageLike { + getItem(key: string): string | null; + setItem(key: string, value: string): void; + removeItem(key: string): void; +} + +export type CaptureProfileStorageErrorCode = + | "unavailable" + | "corrupt" + | "too-large" + | "quota" + | "write-failed"; + +export class CaptureProfileStorageError extends Error { + readonly code: CaptureProfileStorageErrorCode; + + constructor(code: CaptureProfileStorageErrorCode, message: string, cause?: unknown) { + super(message, cause === undefined ? undefined : { cause }); + this.name = "CaptureProfileStorageError"; + this.code = code; + } +} + +export interface CreateCaptureProfileInput { + id?: string; + name: string; + decoderPack: DecoderPackDocument; + settings: CaptureProfileSettings; + createdAt?: string; + updatedAt?: string; + now?: () => Date; +} + +const encoder = new TextEncoder(); + +function defaultStorage(): CaptureProfileStorageLike | null { + try { + return typeof globalThis.localStorage === "undefined" ? null : globalThis.localStorage; + } catch { + return null; + } +} + +function storageFrom( + storage: CaptureProfileStorageLike | null | undefined, +): CaptureProfileStorageLike { + const target = storage === undefined ? defaultStorage() : storage; + if (!target) { + throw new CaptureProfileStorageError( + "unavailable", + "Local capture-profile storage is unavailable. Capture remains usable without saved profiles.", + ); + } + return target; +} + +function errorName(error: unknown): string | null { + if (typeof error !== "object" || error === null || !("name" in error)) return null; + return typeof error.name === "string" ? error.name : null; +} + +function profileId(): string { + const random = globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2); + return `profile-${random}`; +} + +function validateProfile(input: unknown): CaptureProfileDocument { + const result = captureProfileDocumentSchema.safeParse(input); + if (!result.success) { + throw new CaptureProfileStorageError( + "corrupt", + `A capture profile is invalid: ${result.error.issues[0]?.message ?? "schema validation failed"}`, + ); + } + try { + verifyDecoderPackConformance(result.data.decoderPack); + } catch (error) { + throw new CaptureProfileStorageError( + "corrupt", + "A capture profile contains an incompatible or altered decoder pack.", + error, + ); + } + return result.data; +} + +function serializeProfiles(profiles: readonly CaptureProfileDocument[]): string { + const value = JSON.stringify({ + format: "narrowslink/capture-profiles", + formatVersion: 1, + profiles, + }); + if (encoder.encode(value).byteLength > MAX_CAPTURE_PROFILE_STORAGE_BYTES) { + throw new CaptureProfileStorageError( + "too-large", + "Saved capture profiles exceed the 2 MiB local profile budget. Remove a profile or use a smaller decoder pack.", + ); + } + return value; +} + +function parseProfiles(value: string): CaptureProfileDocument[] { + if (encoder.encode(value).byteLength > MAX_CAPTURE_PROFILE_STORAGE_BYTES) { + throw new CaptureProfileStorageError( + "corrupt", + "Stored capture profiles exceed the supported 2 MiB limit.", + ); + } + let input: unknown; + try { + input = JSON.parse(value) as unknown; + } catch (error) { + throw new CaptureProfileStorageError( + "corrupt", + "Stored capture profiles are not valid JSON.", + error, + ); + } + const result = captureProfileCollectionSchema.safeParse(input); + if (!result.success) { + throw new CaptureProfileStorageError( + "corrupt", + `Stored capture profiles are invalid: ${result.error.issues[0]?.message ?? "schema validation failed"}`, + ); + } + const ids = new Set(); + return result.data.profiles.map((profile) => { + if (ids.has(profile.id)) { + throw new CaptureProfileStorageError("corrupt", "Stored capture profile identifiers are duplicated."); + } + ids.add(profile.id); + return validateProfile(profile); + }); +} + +function writeProfiles( + profiles: readonly CaptureProfileDocument[], + storage: CaptureProfileStorageLike, +): void { + const value = serializeProfiles(profiles); + try { + storage.setItem(CAPTURE_PROFILE_STORAGE_KEY, value); + } catch (error) { + if (errorName(error) === "QuotaExceededError") { + throw new CaptureProfileStorageError( + "quota", + "The browser has no space left for capture profiles. Existing capture and session data were not changed.", + error, + ); + } + throw new CaptureProfileStorageError( + "write-failed", + "The capture profile could not be saved locally.", + error, + ); + } +} + +export function createCaptureProfile(input: CreateCaptureProfileInput): CaptureProfileDocument { + const now = input.now?.() ?? new Date(); + const createdAt = input.createdAt ?? now.toISOString(); + const updatedAt = input.updatedAt ?? now.toISOString(); + return validateProfile({ + format: CAPTURE_PROFILE_FORMAT, + formatVersion: CAPTURE_PROFILE_FORMAT_VERSION, + id: input.id ?? profileId(), + name: input.name.trim(), + createdAt, + updatedAt, + decoderPack: input.decoderPack, + settings: input.settings, + }); +} + +export function loadCaptureProfiles( + storage?: CaptureProfileStorageLike | null, +): CaptureProfileDocument[] { + const target = storageFrom(storage); + let value: string | null; + try { + value = target.getItem(CAPTURE_PROFILE_STORAGE_KEY); + } catch (error) { + throw new CaptureProfileStorageError( + "unavailable", + "Local capture-profile storage could not be read. Capture remains usable without saved profiles.", + error, + ); + } + if (value == null) return []; + return parseProfiles(value) + .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt, "en")); +} + +export function saveCaptureProfile( + profile: CaptureProfileDocument, + storage?: CaptureProfileStorageLike | null, +): CaptureProfileDocument[] { + const target = storageFrom(storage); + const validated = validateProfile(profile); + const current = loadCaptureProfiles(target); + const existingIndex = current.findIndex((candidate) => candidate.id === validated.id); + const next = existingIndex >= 0 + ? current.map((candidate, index) => index === existingIndex ? validated : candidate) + : [validated, ...current]; + if (next.length > MAX_CAPTURE_PROFILES) { + throw new CaptureProfileStorageError( + "too-large", + `NarrowsLink supports up to ${MAX_CAPTURE_PROFILES} saved capture profiles.`, + ); + } + next.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt, "en")); + writeProfiles(next, target); + return next; +} + +export function removeCaptureProfile( + profileIdToRemove: string, + storage?: CaptureProfileStorageLike | null, +): CaptureProfileDocument[] { + const target = storageFrom(storage); + const current = loadCaptureProfiles(target); + const next = current.filter((profile) => profile.id !== profileIdToRemove); + if (next.length === current.length) return current; + if (next.length === 0) { + try { + target.removeItem(CAPTURE_PROFILE_STORAGE_KEY); + } catch (error) { + throw new CaptureProfileStorageError( + "write-failed", + "The capture profile could not be removed from local storage.", + error, + ); + } + return []; + } + writeProfiles(next, target); + return next; +} + +export function clearCaptureProfiles( + storage?: CaptureProfileStorageLike | null, +): void { + const target = storageFrom(storage); + try { + target.removeItem(CAPTURE_PROFILE_STORAGE_KEY); + } catch (error) { + throw new CaptureProfileStorageError( + "write-failed", + "Saved capture profiles could not be reset.", + error, + ); + } +} diff --git a/src/capture/index.ts b/src/capture/index.ts index 08b9b8b..ccf0059 100644 --- a/src/capture/index.ts +++ b/src/capture/index.ts @@ -1,3 +1,5 @@ +export * from "./capture-preflight"; +export * from "./capture-profile"; export * from "./nsl01-serial-assembler"; export * from "./recorder"; export * from "./udp-bridge"; diff --git a/src/capture/web-serial.test.ts b/src/capture/web-serial.test.ts index 270b9a5..be19b00 100644 --- a/src/capture/web-serial.test.ts +++ b/src/capture/web-serial.test.ts @@ -193,6 +193,49 @@ describe("WebSerialCapture", () => { expect(close).toHaveBeenCalledOnce(); expect(capture.active).toBe(false); }); + + it("switches future chunks to new handlers without reopening the selected port", async () => { + let controller: ReadableStreamDefaultController | undefined; + const readable = new ReadableStream({ + start(nextController) { + controller = nextController; + }, + }); + const port: WebSerialPort = { + readable, + open: vi.fn(async () => undefined), + close: vi.fn(async () => undefined), + getInfo: () => ({ usbVendorId: 0x1209 }), + }; + const requestPort = vi.fn(async () => port); + const preflight: number[][] = []; + const recording: number[][] = []; + const capture = new WebSerialCapture({ requestPort }); + + await capture.start({ baudRate: 115_200 }, { + onChunk: (bytes) => preflight.push([...bytes]), + }); + controller?.enqueue(Uint8Array.from([1, 2])); + await vi.waitFor(() => expect(preflight).toEqual([[1, 2]])); + + capture.replaceHandlers({ + onChunk: (bytes) => recording.push([...bytes]), + }); + controller?.enqueue(Uint8Array.from([3, 4])); + await vi.waitFor(() => expect(recording).toEqual([[3, 4]])); + await capture.stop(); + + expect(preflight).toEqual([[1, 2]]); + expect(requestPort).toHaveBeenCalledOnce(); + expect(port.open).toHaveBeenCalledOnce(); + }); + + it("rejects handler replacement when no serial port is open", () => { + const capture = new WebSerialCapture({ requestPort: vi.fn() }); + expect(() => capture.replaceHandlers({ onChunk: () => undefined })).toThrow( + "Serial handlers can only be replaced while the selected port is open.", + ); + }); }); describe("getBrowserSerialApi", () => { diff --git a/src/capture/web-serial.ts b/src/capture/web-serial.ts index 2c7e06b..1851f0e 100644 --- a/src/capture/web-serial.ts +++ b/src/capture/web-serial.ts @@ -93,6 +93,7 @@ export class WebSerialCapture { private reader: ReadableStreamDefaultReader | null = null; private readTask: Promise | null = null; private stopTask: Promise | null = null; + private handlers: SerialCaptureHandlers | null = null; private starting = false; private stopRequested = false; @@ -102,6 +103,19 @@ export class WebSerialCapture { return this.port != null && !this.stopRequested; } + /** + * Changes where future serial reads are delivered without reopening the + * operator-selected device. JavaScript callback execution is serialized, so + * this creates an exact boundary between the final old-handler chunk and the + * first new-handler chunk. + */ + replaceHandlers(handlers: SerialCaptureHandlers): void { + if (!this.active) { + throw new WebSerialCaptureError("Serial handlers can only be replaced while the selected port is open."); + } + this.handlers = handlers; + } + async start(options: SerialOpenOptions, handlers: SerialCaptureHandlers): Promise { if (this.port || this.starting) throw new WebSerialCaptureError("A serial capture is already active."); const validated = validateOptions(options); @@ -129,7 +143,8 @@ export class WebSerialCapture { const info = port.getInfo(); const device = { label: formatSerialDevice(info), info }; handlers.onOpen?.(device); - this.readTask = this.readLoop(port, handlers); + this.handlers = handlers; + this.readTask = this.readLoop(port); return device; } catch (cause) { this.stopRequested = true; @@ -143,6 +158,7 @@ export class WebSerialCapture { this.port = null; this.reader = null; this.readTask = null; + this.handlers = null; } throw new WebSerialCaptureError("Serial capture setup failed before reading began.", { cause }); } @@ -190,6 +206,7 @@ export class WebSerialCapture { this.readTask = null; this.reader = null; this.port = null; + this.handlers = null; } } @@ -197,7 +214,7 @@ export class WebSerialCapture { if (readFailure) throw new WebSerialCaptureError("The serial read loop did not stop cleanly.", { cause: readFailure }); } - private async readLoop(port: WebSerialPort, handlers: SerialCaptureHandlers): Promise { + private async readLoop(port: WebSerialPort): Promise { while (!this.stopRequested && port.readable) { const reader = port.readable.getReader(); let streamEnded = false; @@ -209,10 +226,10 @@ export class WebSerialCapture { streamEnded = true; break; } - if (value && value.byteLength > 0) handlers.onChunk(new Uint8Array(value)); + if (value && value.byteLength > 0) this.handlers?.onChunk(new Uint8Array(value)); } } catch (cause) { - if (!this.stopRequested) handlers.onError?.(asError(cause, "Serial read failed.")); + if (!this.stopRequested) this.handlers?.onError?.(asError(cause, "Serial read failed.")); } finally { reader.releaseLock(); if (this.reader === reader) this.reader = null; @@ -221,7 +238,7 @@ export class WebSerialCapture { } if (!this.stopRequested) { this.stopRequested = true; - handlers.onDisconnect?.(); + this.handlers?.onDisconnect?.(); } } } diff --git a/src/styles.css b/src/styles.css index 8478256..fdc1bc1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2650,6 +2650,82 @@ select:focus-visible { margin: 0; } +.capture-profile-picker { + display: grid; + grid-template-columns: minmax(0, 1fr) auto 36px; + align-items: end; + gap: 8px 10px; + padding: 11px 12px; + border: 1px solid var(--line); + background: #101410; +} + +.capture-profile-picker > label, +.capture-profile-editor label { + display: grid; + gap: 6px; + min-width: 0; + color: var(--muted); + font-size: 9px; +} + +.capture-profile-picker select, +.capture-profile-editor input { + width: 100%; + min-height: 36px; + padding: 8px 9px; + border: 1px solid var(--line); + border-radius: 0; + background: #0f1310; + color: var(--text); +} + +.capture-profile-picker > p { + grid-column: 1 / -1; + overflow-wrap: anywhere; + margin: 0; + color: var(--dim); + font-family: var(--mono); + font-size: 8px; + line-height: 12px; +} + +.capture-profile-delete { + width: 36px; + height: 36px; + min-height: 36px; + border: 1px solid var(--line); + border-radius: 0; + background: #151916; + color: var(--muted); +} + +.capture-profile-delete:disabled { + color: #4d554f; + cursor: not-allowed; +} + +.capture-profile-editor { + grid-column: 1 / -1; + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: end; + gap: 8px; + padding-top: 10px; + border-top: 1px solid var(--line-soft); +} + +.capture-profile-storage-error { + grid-column: 1 / -1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + color: #efb46c; + font-size: 9px; + line-height: 14px; +} + .capture-decoder-picker { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -2842,6 +2918,31 @@ select:focus-visible { padding: 0 12px 10px; } +.capture-preflight-assessment, +.capture-evidence-boundary { + margin: 0; + padding: 9px 12px; + border-top: 1px solid var(--line-soft); + font-size: 9px; + line-height: 14px; +} + +.capture-preflight-assessment { + color: var(--muted); +} + +.capture-preflight-assessment.ready { + color: var(--green); +} + +.capture-preflight-assessment.attention { + color: #efb46c; +} + +.capture-evidence-boundary { + color: #b8dce7; +} + .capture-lock-notice { margin-top: 0; color: var(--dim); @@ -2897,8 +2998,15 @@ select:focus-visible { } .capture-dialog-actions { + position: sticky; + z-index: 2; + bottom: -24px; min-height: 36px; align-items: center; + margin: 0 -24px -24px; + padding: 12px 24px 24px; + border-top: 1px solid var(--line-soft); + background: #151916; } .capture-action-progress { @@ -5230,6 +5338,12 @@ select:focus-visible { padding: 20px 16px; } + .capture-dialog-actions { + bottom: -20px; + margin: 0 -16px -20px; + padding: 12px 16px 20px; + } + .capture-session-fields, .capture-transport-fields { grid-template-columns: 1fr; @@ -5239,6 +5353,32 @@ select:focus-visible { grid-template-columns: 1fr; } + .capture-profile-picker { + grid-template-columns: minmax(0, 1fr) 36px; + } + + .capture-profile-picker > label, + .capture-profile-picker > p, + .capture-profile-editor, + .capture-profile-storage-error { + grid-column: 1 / -1; + } + + .capture-profile-delete { + grid-column: 2; + grid-row: 2; + } + + .capture-profile-save { + grid-column: 1; + grid-row: 2; + width: 100%; + } + + .capture-profile-editor { + grid-template-columns: 1fr; + } + .capture-decoder-picker p { grid-column: 1; } diff --git a/tests/e2e/capture-to-evidence.spec.ts b/tests/e2e/capture-to-evidence.spec.ts index 9d1a5bc..cba11d7 100644 --- a/tests/e2e/capture-to-evidence.spec.ts +++ b/tests/e2e/capture-to-evidence.spec.ts @@ -179,7 +179,21 @@ test("captures real NMEA UDP traffic and hands off the exact decoder interpretat await captureDialog.getByLabel("Bridge token", { exact: true }).fill(bridge.token); await captureDialog.getByLabel("UDP bind host", { exact: true }).fill("127.0.0.1"); await captureDialog.getByLabel(/UDP port/).fill("0"); - await captureDialog.getByRole("button", { name: "Start UDP capture" }).click(); + + await captureDialog.getByRole("button", { name: "Save setup" }).click(); + await captureDialog.getByLabel("Profile name").fill("Gig Harbor NMEA UDP"); + await captureDialog.getByRole("button", { name: "Save profile" }).click(); + await expect(captureDialog).toContainText( + "Gig Harbor NMEA UDP was saved locally without bridge credentials, device permissions, or telemetry payloads.", + ); + + await captureDialog.getByRole("button", { name: "Run UDP preflight" }).click(); + await expect(captureDialog.getByRole("region", { name: "Preflight waiting for traffic" })).toBeVisible(); + await bridge.waitForStatus((status) => status.state === "capturing" && (status.udp?.port ?? 0) > 0); + await bridge.sendDatagrams([nmeaHex[0]!]); + await expect(captureDialog.getByRole("region", { name: "Preflight ready" })).toBeVisible(); + await expect(captureDialog).toContainText("Traffic and decoder fit are confirmed."); + await captureDialog.getByRole("button", { name: "Start recording" }).click(); await expect(captureDialog.getByRole("region", { name: "Recording" })).toBeVisible(); const sent = await bridge.sendDatagrams(nmeaHex, { intervalMs: 15 }); @@ -228,6 +242,18 @@ test("captures real NMEA UDP traffic and hands off the exact decoder interpretat runtime: { id: "nmea0183-line-v1", revision: "1" }, }); + await page.getByRole("button", { name: /Live capture UDP or serial/ }).click(); + const reopenedCaptureDialog = page.getByRole("dialog", { name: "Record live telemetry" }); + await reopenedCaptureDialog.getByRole("combobox", { name: "Capture profile", exact: true }) + .selectOption({ label: "Gig Harbor NMEA UDP · UDP" }); + await expect(reopenedCaptureDialog.getByRole("combobox", { name: "Decoder pack", exact: true })).toHaveValue( + portablePack.integrity.canonicalSha256, + ); + await expect(reopenedCaptureDialog.getByLabel("UDP bind host", { exact: true })).toHaveValue("127.0.0.1"); + await expect(reopenedCaptureDialog.getByLabel(/UDP port/)).toHaveValue("0"); + await expect(reopenedCaptureDialog.getByLabel("Bridge token", { exact: true })).toHaveValue(""); + await reopenedCaptureDialog.getByRole("button", { name: "Cancel" }).click(); + const bundlePanel = page.getByRole("region", { name: "Incident bundle preview" }); await bundlePanel.getByRole("button", { name: "Create incident bundle" }).click(); const bundleDialog = page.getByRole("dialog", { name: "Package this incident for handoff?" }); @@ -312,6 +338,38 @@ test("captures real NMEA UDP traffic and hands off the exact decoder interpretat expect(browserErrors).toEqual([]); }); +test("diagnoses a decoder mismatch before recording and discards the probe cleanly", async ({ page }) => { + bridge = await startLoopbackBridge(); + await page.goto("/"); + await page.getByRole("button", { name: /Live capture UDP or serial/ }).click(); + const captureDialog = page.getByRole("dialog", { name: "Record live telemetry" }); + await captureDialog.getByRole("combobox", { name: "Decoder pack", exact: true }) + .selectOption(NMEA0183_DECODER_PACK.integrity.canonicalSha256); + await captureDialog.getByLabel(/Bridge URL/).fill(bridge.controlUrl); + await captureDialog.getByLabel("Bridge token", { exact: true }).fill(bridge.token); + await captureDialog.getByLabel("UDP bind host", { exact: true }).fill("127.0.0.1"); + await captureDialog.getByLabel(/UDP port/).fill("0"); + await captureDialog.getByRole("button", { name: "Run UDP preflight" }).click(); + await bridge.waitForStatus((status) => status.state === "capturing" && (status.udp?.port ?? 0) > 0); + await bridge.sendFixtureDatagrams({ count: 3, intervalMs: 10 }); + + await expect(captureDialog.getByRole("region", { name: "Preflight needs attention" })).toBeVisible(); + await expect(captureDialog).toContainText( + "Traffic is arriving, but the selected decoder has not produced a valid frame.", + ); + await expect(captureDialog.getByRole("button", { name: "Record with warning" })).toBeEnabled(); + await expect(captureDialog).toContainText( + "Preflight samples are not retained. Starting recording stops this probe and opens a new owned capture ID.", + ); + + await captureDialog.getByRole("button", { name: "Stop preflight" }).click(); + const readyStatus = captureDialog.getByRole("region", { name: "Ready" }); + await expect(readyStatus).toBeVisible(); + await expect(readyStatus.getByText("0s", { exact: true })).toBeVisible(); + await expect(captureDialog).toContainText("Its sampled bytes were not added to a session."); + await bridge.waitForStatus((status) => status.state === "stopped"); +}); + test("records UDP, replays and investigates it, then exports independently verifiable evidence", async ({ page }, testInfo) => { const browserErrors: string[] = []; page.on("console", (message) => { @@ -332,7 +390,12 @@ test("records UDP, replays and investigates it, then exports independently verif await captureDialog.getByLabel("Bridge token", { exact: true }).fill(bridge.token); await captureDialog.getByLabel("UDP bind host", { exact: true }).fill("127.0.0.1"); await captureDialog.getByLabel(/UDP port/).fill("0"); - await captureDialog.getByRole("button", { name: "Start UDP capture" }).click(); + await captureDialog.getByRole("button", { name: "Run UDP preflight" }).click(); + await expect(captureDialog.getByRole("region", { name: "Preflight waiting for traffic" })).toBeVisible(); + await bridge.waitForStatus((status) => status.state === "capturing" && (status.udp?.port ?? 0) > 0); + await bridge.sendFixtureDatagrams({ count: 2, intervalMs: 10 }); + await expect(captureDialog.getByRole("region", { name: "Preflight ready" })).toBeVisible(); + await captureDialog.getByRole("button", { name: "Start recording" }).click(); await expect(captureDialog.getByRole("region", { name: "Recording" })).toBeVisible(); await expectFocusInside(page, "dialog"); diff --git a/tests/e2e/comparison-workspace.spec.ts b/tests/e2e/comparison-workspace.spec.ts index 6af5d88..7343ecc 100644 --- a/tests/e2e/comparison-workspace.spec.ts +++ b/tests/e2e/comparison-workspace.spec.ts @@ -70,7 +70,11 @@ async function captureUdpSession( await dialog.getByLabel("Bridge token", { exact: true }).fill(bridge.token); await dialog.getByLabel("UDP bind host", { exact: true }).fill("127.0.0.1"); await dialog.getByLabel(/UDP port/).fill("0"); - await dialog.getByRole("button", { name: "Start UDP capture" }).click(); + await dialog.getByRole("button", { name: "Run UDP preflight" }).click(); + await bridge.waitForStatus((status) => status.state === "capturing" && (status.udp?.port ?? 0) > 0); + await bridge.sendFixtureDatagrams({ count: 1 }); + await expect(dialog.getByRole("region", { name: "Preflight ready" })).toBeVisible(); + await dialog.getByRole("button", { name: "Start recording" }).click(); await expect(dialog.getByRole("region", { name: "Recording" })).toBeVisible(); await bridge.waitForStatus((status) => status.state === "capturing" && (status.udp?.port ?? 0) > 0); diff --git a/tests/e2e/serial-capture-to-evidence.spec.ts b/tests/e2e/serial-capture-to-evidence.spec.ts index 0b7d9c7..368e45d 100644 --- a/tests/e2e/serial-capture-to-evidence.spec.ts +++ b/tests/e2e/serial-capture-to-evidence.spec.ts @@ -114,11 +114,15 @@ test("records fragmented serial input and carries it through durable verified ev await captureDialog.getByRole("tab", { name: "Serial port" }).click(); await captureDialog.getByLabel("Session title", { exact: true }).fill(CAPTURE_TITLE); await captureDialog.getByLabel(/Display timezone/).fill("UTC"); - await captureDialog.getByRole("button", { name: "Select port & start" }).click(); - await expect(captureDialog.getByRole("region", { name: "Recording" })).toBeVisible(); + await captureDialog.getByRole("button", { name: "Select port & preflight" }).click(); + await expect(captureDialog.getByRole("region", { name: "Preflight waiting for traffic" })).toBeVisible(); await expect(captureDialog.getByText(/Serial state:/)).toContainText("open"); const frames = [501, 502, 503, 504, 505].map(heartbeatFrame); + await serial.emit(heartbeatFrame(500)); + await expect(captureDialog.getByRole("region", { name: "Preflight ready" })).toBeVisible(); + await captureDialog.getByRole("button", { name: "Start recording" }).click(); + await expect(captureDialog.getByRole("region", { name: "Recording" })).toBeVisible(); const reads = [ frames[0]!.slice(0, 5), concat(frames[0]!.slice(5), frames[1]!), @@ -144,8 +148,8 @@ test("records fragmented serial input and carries it through durable verified ev parity: "none", stopBits: 1, }], - emittedReads: 4, - emittedBytes: observedBytes, + emittedReads: 5, + emittedBytes: observedBytes + heartbeatFrame(500).byteLength, readerCancellations: 0, portCloses: 0, }); diff --git a/tests/release/capture-to-evidence.spec.ts b/tests/release/capture-to-evidence.spec.ts index f6cd828..8134880 100644 --- a/tests/release/capture-to-evidence.spec.ts +++ b/tests/release/capture-to-evidence.spec.ts @@ -114,7 +114,7 @@ test("unpacked release records UDP and preserves verifiable evidence across repl const originalPort = Number(new URL(appUrl).port); const releaseIdentity = { ...installation.identity }; expect(originalPort).toBeGreaterThan(0); - expect(releaseIdentity.version).toBe("0.1.0"); + expect(releaseIdentity.version).toBe("0.2.0"); expect(releaseIdentity.commit).toMatch(/^[0-9a-f]{40}$/); expect(server.ready).toMatchObject(releaseIdentity); expect(new URL(server.ready.bridgeUrl).hostname).toBe("127.0.0.1"); @@ -136,7 +136,12 @@ test("unpacked release records UDP and preserves verifiable evidence across repl await captureDialog.getByLabel(/Display timezone/).fill("UTC"); await captureDialog.getByLabel("UDP bind host", { exact: true }).fill("127.0.0.1"); await captureDialog.getByLabel(/UDP port/).fill("0"); - await captureDialog.getByRole("button", { name: "Start UDP capture" }).click(); + await captureDialog.getByRole("button", { name: "Run UDP preflight" }).click(); + await expect(captureDialog.getByRole("region", { name: "Preflight waiting for traffic" })).toBeVisible(); + const preflightPort = await boundUdpPort(captureDialog); + await sendReleaseDatagrams(datagrams.slice(0, 2), preflightPort); + await expect(captureDialog.getByRole("region", { name: "Preflight ready" })).toBeVisible(); + await captureDialog.getByRole("button", { name: "Start recording" }).click(); await expect(captureDialog.getByRole("region", { name: "Recording" })).toBeVisible(); const udpPort = await boundUdpPort(captureDialog);