Fix multisource and demod lifecycle issues - #59
Draft
ceane wants to merge 128 commits into
Draft
Conversation
- Fixing issue where pause was advancing by a frame or when reloading hitting pause didn't work
…top of continuing fixes
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.
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Validation
Notes