Skip to content

Fix multisource and demod lifecycle issues - #59

Draft
ceane wants to merge 128 commits into
mainfrom
continuing-multisource-and-demod
Draft

Fix multisource and demod lifecycle issues#59
ceane wants to merge 128 commits into
mainfrom
continuing-multisource-and-demod

Conversation

@ceane

@ceane ceane commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix multisource source-ownership and Mock Tx standby lifecycle behavior.
  • Fix frontend settings accessibility, duplicate sample-rate options, and test harness/typecheck issues.
  • Fix backend handoff proxy target refresh behavior and Rust standby test expectations.
  • Refresh dependency metadata without bypassing the repository's restricted npm remote-package policy.
  • Adding a start page
    • Screenshot 2026-08-16 at 20 06 10

Validation

  • Frontend focused Jest suites: passing
  • Shader tests: 43 passing
  • Rust library tests: 255 passing
  • TypeScript typecheck: passing

Notes

  • React act usage remains on the repository's existing approach.
  • npm remote fetching was not enabled or bypassed.

ceane and others added 21 commits July 21, 2026 18:05
- Fixing issue where pause was advancing by a frame or when reloading hitting pause didn't work
Unify spectrum state ownership, source lifecycle handling, and whole-channel controls across the frontend and backend.

Replace the Mock Tx generators with reusable complex-baseband synthesis, preserve signal shape and bandwidth contracts, normalize power consistently across frontend FFT and Tx IFFT sizes, and cover OFDM variance and integrated large-block power.

Harden SDR hotplug, source swapping, HackRF TX plumbing, and non-blocking RTL-SDR reader cleanup. Refresh integration, unit, build, and GPU-path tests to match the new ownership and streaming behavior.

Validation: cargo test --workspace --all-targets; rustfmt --check on the modified complex-baseband Rust modules.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Device switches and transport warm-up were able to flash Server Down after a
prior live session. Gate unavailable presentation on true post-session control
loss only.

Co-authored-by: Cursor <cursoragent@cursor.com>
Brief socket closes during reconnect were clearing sources and active ids,
which thrashed placeholders and forced source reselection.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a /settings route with a scroll-spy sidebar covering theme, SDR,
login, I/Q capture, and snapshot defaults, plus a new /faq landing page
and reusable link-card components for page footers.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Replaces the FAQ layout's local logo img and back link with the shared
Logo and AppBackButton components, drops the redundant "N-APT FAQ" title,
and adds a Lingo and Learn link to the FAQ home. Adds hideHeader support
to ThemeSection for embedding in the settings page.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Supports externally controlled open state, a section id for sidebar
scroll targets, and an embedded header-free rendering mode.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Adds lazy routes for /settings, /get-started, and a /faq landing page
replacing the old redirect.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- add source-owned multiplexed streaming, presentation control, and TX standby handling\n- expand Rust websocket/source lifecycle, SDR recovery, TX monitor, and stream manager behavior\n- reorganize frontend shell, routes, sidebar layouts, Learn Signals, FAQ content, and visualization flows\n- update Redux/WebSocket contracts, frame processing, frequency controls, WebGPU waterfall behavior, and capture policy\n- add focused frontend, integration, Rust, shader, rebuild-status, and streaming regression coverage\n- move Rust hot-reload helpers and document testing conventions and design decisions\n\nValidation: TypeScript typecheck passed; focused Rust and frontend checks were run. Existing full-suite failures and environment-blocked localhost tests remain documented in the task history.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Comment thread scripts/legal-app-server/index.mjs Fixed
ceane and others added 30 commits August 24, 2026 16:35
- Decode IQ bytes as unsigned offset-binary ((byte - 128) / 128) like
  every other consumer of this stream. The old `as i8` reinterpretation
  flipped the sign of every sample >= 128 and mirrored the spectrum.
- Measure the 3 dB bandwidth around the dominant peak's original bin.
  Round-tripping bin -> Hz -> bin saturated negative-frequency peaks
  (upper half of the FFT) to bin 0, measuring bandwidth around the
  wrong bin.
- Cap retained algorithm results at 256 so long sessions stop growing
  the results Vec (with embedded peak vectors) for the process lifetime.
- Add regression tests for both fixes.
validate_capability no longer grants full receive/transmit/full-duplex
privileges to a source id missing from the capability registry — an
unknown source (typo, stale reference, or crafted input) now fails
subscription with a Capability error. Capabilities are registered
per-source at subscribe time from the device inventory snapshot.

The conservative fallback in mode arbitration also flips to all-false
so an unregistered source is treated as half-duplex rather than
permissive.

Tests: unknown-source subscribe denial for Rx and Tx; existing tests
register the mock source explicitly to mirror the websocket path.
The backend echoes every device-scoped tune back to all subscribers,
including the originator; the echoed authoritative state could then be
re-applied over an in-flight gesture and oscillate between stale
windows.

Backend:
- WebSocketMessage carries an optional origin_id stamped by the tuning
  client; SharedState remembers last_tune_origin_id on device-scoped
  retunes and build_channels_snapshot echoes it so clients can drop
  their own echo while foreign subscribers still apply it.

Client:
- websocketMiddleware stamps outgoing tunes (frequency_range,
  set_frequency_range, demod_tune) with a per-page-load CLIENT_ORIGIN_ID.
- On authoritative non-local stream_options_applied hydration, open a
  750 ms suppression window during which only option sets whose center
  matches the latest outgoing gesture intent may publish; a fresh
  gesture clears the window so legitimate fast retunes are never
  suppressed.
- The decision is the pure predicate shouldSuppressRxOptionsCandidate
  in model/multiplexStream/presentationGate.ts; the middleware only
  applies it. Documented as shipped in the pipeline architecture doc;
  invariant: one writer, one current tune — hydration may rewrite
  Redux, never the device.

Covered by multiplexStreamPresentationGate tests for the predicate.
frameRuntime gains resolveFrameSlot: a single decision ladder behind
every source-scoped frame ref lookup, replacing ad-hoc per-proxy map
reads. Tier order:

1. Active presentation target — only when no explicit mode is requested
   or the modes agree, and the presentation ref holds content or the
   slot exists.
2. The requested source/mode slot exclusively; a lifecycle-current
   frozen frame outranks the live ref. Consumers never cross the RX/TX
   mode boundary while switching back to a source.
3. Legacy source-scoped fallbacks — safe only without an explicit mode,
   otherwise an RX canvas could be handed a TX preview from the same
   physical device during a mode switch.

Every read/write records which tier served it (FrameSlotResolutionKind).
WaterfallNode drops its dataFrameCounter subscription and re-read
effect: frame revisions now flow through the ladder, so the counter no
longer needs to force file-mode refreshes.

Behavior characterized by new tests in test/ts/frameRuntime.test.ts.
- Add resolveEdgeClampedCenterHz: entering a spectrum bound corrects
  the center so the window's edge lands on the bound instead of pushing
  half the window past it (which the backend rejects).
- EditableCenterFrequency takes windowSpanHz and applies that clamp on
  commit, including the negative ceiling in mirror mode; WaterfallNode
  passes the current acquisition span.
- useSpectrumInteraction never leaves the mirrored acquisition
  unbounded: before hardware bounds hydrate (cold start), a deep
  negative pan used to fold to a positive window the backend rejected,
  freezing the UI. Bounds now pass through getAvailableSpectrumBounds.
- useStitchingLogic reads the range from a ref so the stitch-complete
  callback no longer depends on (and resets with) every frequencyRange
  change.
- Cap the stitch session cache at 3 sessions, evicting the oldest.

Covered by new tests for resolveEdgeClampedCenterHz and the clamped
commit path.
SpectrumRoute had grown to ~2600 lines with snapshot history, fast
snapshot modes, TX hop preview, note view history, and live tuning
state all inline. Extract each concern into a hook under
app/routes/pages/spectrum/hooks/:

- useFastSnapshotControls: fast snapshot mode tiers + FFT snapshot
  loading (FastSnapshotControl UI state moves out of the route).
- useTxHopPreview: mock TX monitor preview request dedupe and preview
  window lifecycle.
- useNoteViewHistory: note card stats snapshot history.
- useLiveTuning: useFrequencyTuning/useTxMonitor — center frequency,
  range resolution, and TX monitor tuning.

The route now composes the hooks; helpers still re-exported for tests.
No behavior change intended.
- New nodeRegistry: eager imports for the small always-present nodes,
  React.lazy + Suspense for the heavy ones (CoreML, spike detection,
  beat, FFT, waterfall, spectrogram, channels, analysis, APT, FM, file
  options, radio, stream, tempo note, IQ capture, TX, bitstream viewer,
  symbols table). CustomNode resolves content via resolveDemodNodeEntry
  instead of the 25-branch if-chain, with a shared fallback and
  Suspense boundary per node.
- Demod auto-layout runs in demodLayoutWorker (elkjs bundled import),
  driven by demodLayoutClient's runId-matched request/reply protocol;
  ELK never loads on the main thread. The CJS/ESM interop shim
  (resolveDemodElkConstructor) moves into the worker and out of the
  flow model.
The 3-2-1 capture countdown lived on AnalysisSession, so every tick
re-rendered every consumer of the analysis context. It now lives in a
dedicated DemodCaptureCountdownContext (provider + useDemodCaptureCountdown)
scoped under the analysis provider; VisionScene subscribes to it
directly and AnalysisSession drops its countdown field. Reset clears
the countdown alongside the session.
When the Vite readiness probe fails during a dev build, start a small
standalone HTTP server (devStatusServer.ts) that serves a themed
prelude status page (n-apt-dev-status marker meta) built from
THEME_TOKENS, so browsers pointed at the dev URL see live build state
instead of a connection error.

- build-orchestrator (TUI and non-TTY paths) starts the status server
  on probe failure, closes it once Vite comes up or before relaunching,
  and shuts it down with the other tracked children.
- Non-TTY runs now persist step-by-step RebuildStatusPayload to
  .rebuild-status.json; cargoBuildProgress gains the RebuildStatusStep
  shape shared by both writers.
- vite.config.js picks up the status endpoint wiring.
- setup_test_server now returns the spawned Redis URL so tests can
  inspect live session state.
- Full password auth flow: challenge -> HMAC proof with the
  password-derived key -> verify, pinning that wrong-password proofs
  are rejected (and consume the challenge), and that a correct proof
  issues a token that authenticates protected endpoints.
- Session lifecycle: validate returns the exact per-session AES key
  material stored at create time; revoke invalidates it and is
  idempotent for well-formed tokens.
- Pin the current at-rest representation: sessions are plaintext JSON
  with an encryption_key byte array in Redis DB 1 — fails on purpose if
  this ever moves to encrypted-at-rest.
- Vault key accepts the Bearer Authorization header form.
Logs are noise at this stage; drop them and keep genuine failure
signals.

- useAuthentication: reducer/init/session logs, WebAuthn-disabled
  warns, localStorage-fallback debugs, and the now-unused
  hasLoggedWebAuthnIdeNoticeRef.
- ReduxProvider: init log, persistence-error log, and the pointless
  __reduxProviderInitialized global (also dropped from global.d.ts).
- auth service: passkey registration flow logs and duplicate
  console.error alongside thrown errors.
- TransformersRoute: model loading/analysis progress logs and error
  logs (errors already surface in UI state).
- SpectrumSidebar: PlaybackAfterCapture console.group flow logs,
  auth-wait and fetch-retry logs (thrown errors kept).
- Waterfall draw hooks and pause logic: dev-only validation metadata
  dumps; validation warnings remain.
- useSharedBufferManager: GC log. useSpectrumStore: [pause-debug] logs.
- Math components: dev-gated sanitize debug dumps in SafeBlockMath/
  SafeInlineMath; removed unused sanitizeLatexWithDebug helper.
- EndpointsListAndSearch: placeholder "navigate to endpoint" log;
  handler documents that map centering is not wired yet.

Kept: the opt-in __NAPT_PROFILE_SNAPSHOT__ profiler, console.warn on
real warnings, stories files.
cleanupSocket detached handlers and called close() unconditionally. A
WebSocket in CONNECTING state cannot abort its handshake; calling
close() there makes the browser log "WebSocket is closed before the
connection is established" on every fast unmount/reconnect cycle.

Now: take the socket out of wsInstance first, detach all handlers, and
only close once it has settled — a CONNECTING socket gets a one-shot
open listener that closes it when the handshake completes, so the
pending open stays inert (no state dispatches) instead of being
aborted mid-handshake.
Clicking an endpoint in the endpoints search list now sets it as the
map preview location: MapEndpointsRoute re-centers on previewLocation
and renders its marker at zoom 16, without persisting anything to the
saved-locations list. The handler was previously a no-op placeholder
after its console.log was removed.
- test:iq-capture pointed at a removed shell wrapper; now runs the
  iq_capture_integration_tests cargo suite directly
- towers:download:opencellid pointed at a file that never existed in
  scripts/redis; repointed to the cached downloader
- redis:start/stop/restart/status referenced scripts/redis/setup_redis.sh
  which only existed under scripts/data; moved the improved cached
  downloader (empty-DB guard) and setup_redis.sh into scripts/redis
- removed byte-identical or stale duplicates of the tower/redis tooling
  from scripts/data (canonical copies live in scripts/redis) and the
  duplicate check_changes.sh / kill_blockers.sh from scripts/processing
- delete s/fft/mod_rust.rs (never declared in mod.rs)
- prune dsp::fft / dsp::simd re-export arms; dsp now only re-exports the
  complex_baseband shim actually used by tx/ifft
- drop dead inherent RtlSdrDevice::get_center_frequency duplicate of the
  SdrDevice trait method; drop unused HackRf async_thread field
- remove uncalled server utils downsample_spectrum /
  extract_channels_from_value and the never-constructed types.rs
  MockAptSignal + SignalType pair
- delete unused MetalBackend::is_available, AlgorithmTester result
  accessors and the never-constructed AnalysisResult::Custom variant
- scalar SIMD fallbacks now carry their real cfg gate instead of
  allow(dead_code); stale allows removed where items are in use
- live_stream_test bin annotates its shared crypto include so
  unused-in-binary warnings are expected rather than suppressed per item

cargo check --workspace is now warning-free
Removes 57 files that no production entry point (SPA index.html -> Main,
RR7 root/routes, workers, CLI) can reach, verified by import-graph scan:
- legacy app barrel + orphan hooks/services that only its dead branches
  used (useThemeStore, services/env, rebuildStatusMessage stays restored
  for the live SPA entry)
- 9 unused spectrum hooks incl. the duplicated waterfall buffer pool
- 5 shader-string modules superseded by the canonical WGSL sources
- capture sidebar chain, learn/pretext stragglers, demod barrels and
  harness stubs, draw-signal and maps demos, UI leftovers

Test updates:
- shader fixtures are now generated from src/ts/shaders/*.wgsl directly
  (the files that ship) instead of drifted string copies; two shaders
  had silently diverged from their tested copies
- tests of deleted modules removed; ThemeSection/cliSnapshotHarness/
  useMapLocations/DeviceStreamFrozen/FFTCanvasPause updated to drop
  mocks of modules prod no longer imports
- routes.ts logout route pointed at the file location that exists

typecheck, jest (285 suites), vitest shader suite, vite build and
react-router build all pass
MarkdownRenderer, CanvasComponents, MultipathCanvas and helpers are not
imported by the article build (vite.markdown.config.ts entry graph).
The demod analysis start path ran a fabricated sequence: a cosmetic
3-second countdown, then for APT baselines a purely synthetic progress
animation ending in a Math.random 'result' (confidence/matchRate/SNR).
Real captures were also polluted with random quality metrics.

- DemodContext.startAnalysis now dispatches the real encrypted capture
  command immediately; phase transitions come from actual websocket
  captureStatus messages plus the requested-duration timer
- fake confidence/matchRate/snrDelta generation deleted and the
  CaptureResult fields made optional; AnalysisNode renders SNR/summary
  only when present
- dead apt_result window listener removed (middleware emits
  aptAnalysisResult; nothing ever consumed it)
- StimulusNode drops the countdown UI and the APT preview option whose
  only behavior was simulation; capture progress bar stays (driven by
  real capture state)
- DemodCaptureCountdown context plumbing removed; VisionScene no longer
  depends on it
- FileWorkerManager now rejects and clears every pending request when
  the worker errors or a message fails to deserialize, so callers fail
  fast instead of hanging for the full timeout while their transferred
  IQ ArrayBuffers stay pinned by pending closures
- SessionStore.get_conn serializes reconnection through an async lock
  with a cache re-check, so callers racing after an eviction share one
  Redis dial instead of stampeding
- lint:structure script walks the import graph from every real entry
  point (SPA html entry, RR7 route tables, workers, CLI, stories/tests)
  and fails when a module becomes unreachable or a package.json script
  points at a missing file; wired into the lint job
- rust-tests job now compiles with RUSTFLAGS=-D warnings so dead code
  and unused imports fail CI instead of accumulating silently (full
  workspace already passes)
- fixed the two warnings this uncovered: never_loop in hotplug USB scan
  (first-element semantics preserved) and an unused import/binding
Scrolling past −A.max (~−5 MHz) was treated as an uncovered paint and a corrupt pan, which replaced the signed viewport with the positive |f| image and dropped Channel A’s highlight.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants