The user keeps their own vite dev server running all the time on port 3000 (npm run dev). Port 3002 is Claude's β use it for any test/verification instance (npm run dev:test).
- To stop a server, find the exact listener PID (
ss -tlnp | grep 3002) and kill only that PID. Neverpkill -f vite(kills the user's server too) and neverlsof -ti :PORT | xargs kill(lsof also lists client-connection PIDs β this has killed the user's browser). npm run build(tsc -b && vite build) is safe to run alongside a live dev server β Vite's dev server and its build don't share on-disk state the way Next's.next/did, so there's no dev/build cache-corruption class of bug here. If you hit stale-module weirdness in dev, clearingnode_modules/.viteand restarting the dev server is the Vite-equivalent fix, but this hasn't been needed in practice yet.- The app registers a PWA service worker; browsers can serve stale chunks after changes. If the UI looks partially updated, unregister the service worker + clear Cache Storage + hard reload before debugging.
FT8 decodes via ft8mon (vendored, patched, at lib/ft8mon), FT4 via ft8_lib (submodule at lib/ft8_lib); wrappers live in lib/wasm_build/. Any change to lib/ft8mon/, lib/ft8_lib, or lib/wasm_build/*.c* is not complete until the WASM is rebuilt and the artifacts under public/wasm/ are committed β the TS side loads those binaries, not the sources.
Rebuild (from project root; Docker required, FFTW build is cached after the first run):
docker run --rm -v "$(pwd):/src" -w /src/lib/wasm_build -u "$(id -u):$(id -g)" emscripten/emsdk makeRegression benchmark against ft8_lib's reference WAVs (expected ballpark: ft8mon β310/353 matched, ft8_lib β257/353):
docker run --rm -v "$(pwd):/src" -w /src/lib/wasm_build -u "$(id -u):$(id -g)" emscripten/emsdk make test-modules
node lib/wasm_build/testbuild/test_decode.mjs 2Gotchas already learned the hard way (don't re-litigate): ft8mon needs STACK_SIZE=8388608 (ldpc_decode overflows the 64KB default); no pthreads/SharedArrayBuffer (GitHub Pages can't serve COOP/COEP) β ft8mon's entry() runs go() synchronously under #ifdef __EMSCRIPTEN__; keep all ft8mon patches inside #ifdef __EMSCRIPTEN__ guards. A running decode worker holds the old WASM β use the β³ WASM button or reload the page after rebuilding.
npm run test:perf (scripts/perf-testbed.ts) is the canonical heavy-load testbed: headless Firefox (playwright-core; never Chrome β user rule), synthetic decode windows through the dev-only __ftInjectWindow hook, main-thread blocking measured via heartbeat gaps (Firefox has no Long Tasks API). --cat additionally connects the mock uSDX (src/lib/cat/mockSerial.ts) and reports poll-cadence stretching. Requires a dev server running a development build (the hook is tree-shaken from production) β start it with npm run dev:test (port 3002). Port 3000 is the developer's own always-running server; no test or tool may ever bind or assume it.
Golden regression profiles and the numbers any UI/pipeline change must hold (see README "UI performance testbed" for the table): target 50/12sβ1200 contacts β€~150 ms worst freeze; stress 100/8sβ1200 contacts, DOM stays ~4k; medium 18/2.5sβnear-zero blocking.
Hard-won rules for running tests:
- Never edit app source while a test runs against the dev server β HMR reloads the page mid-run, resets its state, and silently invalidates the data. Land all changes, typecheck, restart the dev server, then measure.
- Clean up between takes or the machine starves: kill only OUR processes β match
ms-playwrightpaths or the harness script name and filter to the actualnode/browser PIDs. Neverpkill -f firefoxpatterns that catch the user's/usr/lib64/firefox, neverlsof -ti :PORT | xargs kill(kills client connections, has killed the user's browser). - playwright
page.evaluateunder tsx: pass browser-side code as strings (esbuild injects a__namehelper into serialized functions that doesn't exist in the page); a string pageFunction with an arg is evaluated as an expression and silently no-ops β bake payloads in withJSON.stringify. - All test tooling is TypeScript. No Python in the codebase (user rule).
- Manual live-signal testing (WebSDR β virtual sink β app mic) is documented in the README appendix; prefer a real display over xvfb for decode-quality comparisons (no GPU/vsync skews the decoder's CPU budget).
This repo pins its Node version in .nvmrc (currently v26.3.0). Before running any node/npm/npx command in this repo, run nvm use (or source ~/.nvm/nvm.sh && nvm use if nvm isn't already loaded in the shell) so the command runs under the pinned version, not whatever Node happens to be active. Don't assume the ambient shell's Node matches β check with node --version if unsure. If the pinned version isn't installed via nvm, install it (nvm install) rather than falling back to a different version.
This repo vendors the uSDX BLACK_BRICK radio firmware at:
firmware/usdxBLACKBRICK/usdxBLACKBRICK.ino
Any edit to this file is not complete until it has been compiled, flashed to the physical radio, and validated. Do not consider a firmware change "done" just because the source file was edited β treat compile+flash+test as part of the change itself, the same way a code edit isn't done until it typechecks.
Do not attempt to flash blind. Before running avrdude, confirm:
-
Programmer connected β a USBasp-compatible programmer must be present:
lsusb | grep -i "16c0:05dc"If not found, stop and ask the user to connect the USBasp programmer to the target ATmega328P before proceeding.
-
CAT serial port present (needed for post-flash CAT validation) β typically
/dev/ttyACM1at 38400 baud. Check with:ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null -
ALWAYS ask the user for explicit confirmation before flashing β no exceptions. Flashing overwrites the running firmware on physical hardware and is not easily reversible if something goes wrong mid-write. This applies every single time, even if: the compile step succeeded cleanly, a previous flash in the same session went fine, the change looks trivial, or the user has already approved flashing earlier in the conversation. Approval for one flash does not carry over to the next β ask again each time avrdude is about to run. Do not rationalize skipping this step under time pressure or because the fix seems obviously correct.
cd firmware
arduino-cli compile --fqbn arduino:avr:uno --output-dir ./usdxBLACKBRICK/build ./usdxBLACKBRICK(arduino-cli requires the sketch folder name to match the .ino file inside it and must be invoked from its parent directory β running it from inside usdxBLACKBRICK/ itself fails with "no such file or directory".)
A clean compile is a prerequisite for flashing β do not flash if this errors or warns about overflow.
cd firmware/usdxBLACKBRICK
avrdude -c usbasp -p m328p -B 4 -v \
-U flash:w:./build/usdxBLACKBRICK.ino.hex:i \
-U eeprom:w:./build/usdxBLACKBRICK.ino.eep:i- CAT protocol unit tests (pure logic, no hardware, run any time):
npm test -- src/lib/cat/__tests__/protocol.test.ts. If the firmware change touches a CAT command's format, range, or semantics, update this test file to match β it must reflect actual firmware behavior, not the wire-format spec alone (e.g. a command can accept a value the running build never meaningfully distinguishes β check the firmware source, not just the inline comment on the command handler). - CAT hardware test bed against the physical radio (run after every flash):
npm run test:cat-hardware -- [/dev/ttyACM1] [baud]. This is a TypeScript script (scripts/cat-hardware-test.ts, run viatsx) that talks to the real serial port and validates the IF frame, the full batched multi-command poll, and a SETβGETβrestore round-trip. Don't rely on the unit tests alone to sign off a firmware change β they validate the JS-side parsing, not that the flashed.hexactually behaves as documented. All test bed tooling in this repo is TypeScript β do not write ad hoc Python (or other language) scripts for hardware validation; extendscripts/cat-hardware-test.tsinstead. - Full app test/build gate if the change affects
src/lib/cat/useRadioCAT.tsorsrc/components/RadioCATPanel.tsxtoo:npm test,npx tsc -b --noEmit,npm run build.