c8b6f2d536f39fd0d030e0d8755d98bcf6816a21
193
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c8b6f2d536 |
[fix](trx-frontend-http): stop freezing the page in the browser cache
CI / lint (pull_request) Successful in 2m22s
CI / test (pull_request) Successful in 8m37s
CI / frontend (pull_request) Successful in 4m33s
CI / reuse (pull_request) Successful in 6s
CI / lint (push) Failing after 14m3s
CI / test (push) Successful in 8m12s
CI / frontend (push) Successful in 3m39s
CI / reuse (push) Successful in 6s
index.html, the stylesheets and the entry bundles are all served from fixed URLs and answered with `public, max-age=31536000, immutable`. Nothing in those URLs changes when the bytes behind them do, and immutable tells the browser not to ask, so a client that visited once could go on running the page it downloaded then — for a year, with the build-stamped ETag never consulted. That is how a layout fix ships and one browser still shows the old behaviour while every other one has it: not a rendering difference, a copy of last week's stylesheet. Only the shared chunks are content-addressed — esbuild hashes their names — so only they can be kept forever. Everything served from a stable URL now answers `no-cache`, which asks and gets a 304 in the ordinary case, at the cost of one conditional request per asset per load. Vendored files with a version in the URL stay immutable; Leaflet, whose URL does not name its version, revalidates with the rest. Covered both ways: a unit test on the policy each asset gets, and endpoint tests that the page, the stylesheet and app.js come back revalidating with an ETag, and that an unchanged one answers 304 under the same policy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
17300170cc |
[test](trx-frontend-http): read the statistics counters past their formatting
CI / test (pull_request) Successful in 7m52s
CI / lint (push) Successful in 2m19s
CI / lint (pull_request) Successful in 2m23s
CI / frontend (pull_request) Successful in 4m31s
CI / reuse (pull_request) Successful in 5s
CI / test (push) Successful in 7m43s
CI / frontend (push) Successful in 3m41s
CI / reuse (push) Successful in 6s
The new assertion parsed the decode counter with Number() on its text. The counters are written with toLocaleString(), so the 1220 records the history fixture serves arrive as "1,220" and the parse gave NaN. It passed here and failed on CI because the count in the local repro was 18 — below the point where grouping appears — and the runner's locale groups where mine did not. Read the digits and ignore the separator, whichever one the locale picks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
e41c13917d |
[fix](trx-frontend-http): put HF APRS on the map, under its own source
The HF APRS list never plotted anything. Its plugin ships in the map plugin group and its packets carry positions, but nothing ever handed one to the map, so a station heard on 30 m appeared in the panel and nowhere else — and unlike the replay gaps around it, no reload brought it back. Plot it, and not as more VHF APRS. HF is a different band and a different path, and lumping the two together would leave no way to tell them apart or to look at one without the other, so it goes on as a source of its own: its own colour, its own chip in the map's Show filter, its own entry in the source legend, and its own clear. Station entries are keyed by source and callsign rather than callsign alone, so a station worked on both bands keeps a marker for each while the popups, the search text and the tracks still show the callsign as heard. decode-flow feeds an HF beacon alongside the VHF one and checks all of it: both reach the map under their own sources, the Show row offers HF APRS next to APRS, and turning that chip off takes the HF station off the map while the VHF one stays. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
ae7df31d91 |
[fix](trx-frontend-http): replay what arrived before a lazy view loaded
Opening the Map or Statistics page showed only what had been decoded since the moment it was opened, and a reload — landing straight on the tab, so its module loads at startup ahead of the history — was the only way to see the rest. Two things were being thrown away. The decode log the Statistics page counts lives in the map module, which is lazy. Recording into a module that is not loaded yet is a no-op, and unlike the map markers nothing replayed the log when it finally arrived, so every decode heard before the first visit was simply never counted. Hold those records in the client and hand them over when the module attaches. The map's own replay covered APRS, AIS and VDES, whose plugins implement syncMap, but not the grid squares: the FTx family and WSPR plotted locators as decodes arrived and had no replay at all, so everything they heard before the map loaded was lost, and the unique-grid counter with it. Both plot through a helper now, which their syncMap replays oldest first. A replayed WSPR spot carries the frequency it was heard on rather than one worked out against wherever the dial has moved to since. Pinned in decode-flow, whose history fixture gains FT8 and WSPR spots: after a first visit the statistics count every stored decode and every grid square, which before this change were 0 and 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
bf3bcc8a84 |
[fix](trx-frontend-http): show one rig at a time on the digital modes page
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 8m34s
CI / test (push) Successful in 7m47s
CI / frontend (pull_request) Successful in 4m28s
CI / reuse (pull_request) Successful in 5s
CI / lint (push) Successful in 2m21s
CI / frontend (push) Successful in 3m37s
CI / reuse (push) Successful in 6s
A client connected to several rigs decodes all of them at once, and the decode stream carries every rig's traffic to every browser. The decoder panels listed all of it: a station a background rig copied on another band appeared in the APRS list next to the selected rig's, the vessel counts and the "latest seen" lines counted both, the status lines said "Receiving" because some other rig was, and the CW pane interleaved two rigs into one stream of text that read as neither. The page is about the rig the operator selected — the one whose spectrum is on screen and whose audio is playing — so each panel now shows what that rig heard: rows, counts, latest-seen, status, the live picture a WEFAX or SSTV frame is painting, and the CW pane. Nothing is dropped on the way in. The map is the whole station's view, has its own rig filter, and would empty out if the plugins stopped feeding it, so the histories still hold every rig and the map still plots them. That also means a switch loses nothing: the runtime gained a rerender hook, which the rig switch calls, and switching back brings the other rig's traffic up again. The CW pane is the exception — a running stream of text cannot be unpicked after the fact — so it starts empty on the rig switched to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
86dd36312e |
[test](trx-frontend-http): type the frequency instead of filling it
CI / lint (pull_request) Successful in 2m20s
CI / test (pull_request) Successful in 8m32s
CI / frontend (pull_request) Successful in 4m27s
CI / reuse (pull_request) Successful in 6s
CI / lint (push) Successful in 2m26s
CI / test (push) Successful in 7m42s
CI / frontend (push) Successful in 3m35s
CI / reuse (push) Successful in 5s
tune-links drove the dial with Playwright's fill(), which writes a value into the field without a keystroke. The app arms its guard against its own refreshes on the first keydown, so a filled field stays unguarded: any state update landing between the fill and the Enter rewrites the field with the frequency the radio is already on, and the Enter then re-applies that. The window is a few milliseconds wide on a developer's machine and wide enough to lose on a loaded CI runner, where the test failed claiming the tuning had landed on the frequency it started from. Type it the way an operator does: select the field, then send the characters as keystrokes. The select arms the guard before a single character changes. Under CPU throttling that reproduced the failure — 1 in 6 runs with fill(), on this branch and on main alike — typing came through 8 runs clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
15ff686542 |
[fix](trx-frontend-http): keep the mini views to the rig on screen
The decode SSE stream and the history behind it are not rig-scoped: every rig's decodes reach the browser, each carrying the rig that heard it. The panels on the decoder tabs want that — they aggregate the whole station — but the mini views over the waterfall caption the spectrum underneath, and they were reading the same unfiltered histories. A background rig copying APRS on another band put its frames over the active rig's waterfall. The mode gate did not help: it reads the mode of the rig on screen, so those frames appeared whenever that rig happened to be in PKT. Filter each overlay on the rig it belongs to, through one shared predicate that compares a decode's rig_id with the per-tab active rig already driving the spectrum and the audio. A decode that names no rig, and a session that has not learnt its rig list yet, still show everything. The FTx normalizer was dropping rig_id on the floor, so it now keeps it. CW needed more than a filter: its lines accumulate character by character, so two rigs copying at once braided their text into one unreadable line. Lines in progress are now kept per rig. The bar repaints in render() move into refreshDecodeBars(), which the rig switch calls as well — otherwise the outgoing rig's frames stayed on screen until the next state update — and which finally includes the CW bar. Closes #49 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
b78c4a4dd4 |
[feat](trx-rs): make spectrum affordable over a slow link
CI / lint (pull_request) Successful in 2m22s
CI / test (pull_request) Successful in 8m36s
CI / frontend (push) Successful in 3m38s
CI / reuse (push) Successful in 6s
CI / frontend (pull_request) Successful in 4m27s
CI / reuse (pull_request) Successful in 6s
CI / lint (push) Successful in 2m21s
CI / test (push) Successful in 7m48s
Spectrum dominates the server↔client connection, and all three things that govern its cost were working against a poor link. **It was polled, one round trip per frame.** The client asked for a frame every 50 ms on a dedicated connection and waited for the reply, so the frame rate was capped at 1/RTT — on a 200 ms link, five frames a second no matter what was configured. Add SubscribeSpectrum alongside the existing SubscribeMeter: the server pushes frames from a per-rig broadcast that rig_task fills only while somebody is subscribed. A server too old to know the command answers with an error and leaves the connection usable, so the client falls back to polling on the same connection without reconnecting. **Bins were JSON floats.** 1024 bins spelled out as decimal text is around 10 KB a frame, ~200 KB/s at full rate — while the very next hop, client to browser, already sends the same information as base64 i8 in about 1.4 KB. Bins now travel base64-encoded whole dBFS, the resolution the display draws at anyway. Decoding still accepts the old array form. **Nothing was tunable.** [sdr].spectrum_fft_size and [sdr].spectrum_interval_ms replace the compile-time FFT size and cadence; [[remotes]].spectrum_interval_ms lets the client ask for less. 512 bins at 5 frames/s is roughly 3.5 KB/s against roughly 200 KB/s before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
f396e8f235 |
[fix](trx-frontend-http): give satellite passes their own page
Pass predictions were a third view inside the Weather Satellite Decoder card, under Digital modes — a planning tool filed behind a decoder toggle, beside the FT8 and WEFAX panels it has nothing to do with. Nothing about knowing when a bird comes over belongs there. Move them to /satellites, reached from Tools alongside Statistics, Recorder, Settings and About: occasional destinations that live behind that menu rather than taking a slot in the operating strip. Adding a sixth strip button wrapped the phone nav onto two rows and cost the desktop strip its labels at 1280px, so the tab is hidden from the strip exactly the way its four peers already are — the nav is byte-for-byte what it was. The prediction code moves out of sat.ts into its own plugin that loads with the page, so the decoder card no longer carries it. Countdowns stop when the page is hidden and each visit reloads, since passes go stale while it is closed. The server grows a /satellites index route so a deep link or a refresh serves the SPA shell rather than a 404. Closes #47 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
46c9827e8a |
[fix](trx-config): keep the generated example off the machine that made it
CI / lint (pull_request) Successful in 2m17s
CI / frontend (pull_request) Successful in 3m28s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m16s
CI / test (pull_request) Successful in 7m50s
CI / frontend (push) Successful in 4m11s
CI / reuse (push) Successful in 9s
CI / test (push) Failing after 17m15s
The example is generated from the config defaults, and [decode_logs].dir defaults to the running user's cache directory. So the file rendered /Users/sjg/Library/Caches/trx-rs/decoders on the machine that generated it and /root/.cache/trx-rs/decoders in CI, and the up-to-date test failed for everyone but its author. Pin dir to an illustrative /var/lib/trx-rs/decoders in the example config; omitting the key still falls back to the per-user directory. A new test asserts the rendered example contains none of this machine's home, cache or config directories, so the next environment-derived default cannot slip through the same way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
0fc977f19f |
[style](trx-config): apply rustfmt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
084f629b5b |
[docs](trx-rs): record the trx-config crate and its commands
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
bc63ded583 |
[feat](trx-config): warn about deprecated configuration keys
Several keys quietly stopped doing what they look like they do, and nothing said so: [remote] and the flat per-rig sections are ignored outright once [[remotes]] / [[rigs]] exist, [frontends.rigctl].port and --rigctl-port have been dead since rig_ports replaced them, [frontends.audio].rig_ports is superseded by rig_urls, and default_rig_id was renamed to default_rig_name. Warn once at load, naming the replacement. Defaults are indistinguishable from explicit values after deserialization, so the loader now records which key paths the file actually set and the checks work off that — no warning for a setting the user never wrote. The single-rig flat layout is not deprecated: it is the documented simple form, and only draws a warning when [[rigs]] is silently shadowing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
cfaeb6ee15 |
[docs](trx-rs): generate the example config and correct the manual
trx-rs.toml.example was maintained by hand and had fallen well behind: no
[[rigs]], no [[remotes]], no [timeouts], no bandplan or decode-history
settings, and a [frontends.http].default_rig_id that had been renamed.
Generate it from the config structs instead, so a new field shows up the moment
it exists, and add a test that fails when the checked-in copy drifts:
cargo run -p trx-config --example generate_example
Section comments come from a small table; a section without an entry is still
emitted, so forgetting a comment can never drop a setting from the example.
The manual was wrong about the basics. It listed five config search paths, none
of which the loader has ever looked at (the real order is ./trx-rs.toml → XDG →
/etc), called --print-config output "fully commented" when it carries no
comments at all, and documented a TRX_PLUGIN_DIRS variable no code reads. It
also still described [frontends.rigctl].port as the bind port years after
rig_ports replaced it. Fixed, and the new configuration features are written
up alongside.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
|
||
|
|
76bcce8c54 |
[feat](trx-config): let secrets live outside the config file
Tokens and passphrases had exactly one representation: plain text in
trx-rs.toml. That is awkward for config-management tools, for a config kept in
a private repo, and for anything shared between machines.
Two alternatives:
- ${VAR} anywhere in a config string, expanded from the environment at load.
An unset variable is an error rather than an empty string — a silently blank
passphrase is how authentication gets disabled by accident.
- A *_file sibling for every credential: [listen.auth].tokens_file,
[[remotes]].auth.token_file, [frontends.http.auth].rx_passphrase_file and
.control_passphrase_file, [frontends.http_json.auth].tokens_file. Setting
both forms is an error rather than a guess about which wins.
Plus a nudge: a config file that holds credentials inline and is readable by
group or others gets a warning naming the chmod that fixes it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
|
||
|
|
88ed3da6cc |
[feat](trx-server): make the decoder set configurable per rig
Every rig started nine decoders — APRS, HF APRS, CW, FT8, FT4, WSPR, LRPT,
WEFAX, SSTV — whether or not anyone ever looked at the results. Two rigs on a
Pi meant eighteen decoder tasks chewing CPU for modes the operator does not
run. Only the SDR virtual channels had a decoder list; the analog path had no
say at all.
Add [decoders] per rig:
[decoders]
enabled = ["cw", "ft8", "wspr"]
output_dir = "/var/lib/trx-rs"
using the same decoder names as [sdr.channels].decoders, so there is one
vocabulary. enabled defaults to every decoder, so upgrading changes nothing.
An unknown name is a config error rather than a silently ignored entry.
output_dir also replaces the hard-coded cache paths for the decoders that write
images, so SSTV, WEFAX and LRPT output can live somewhere the operator chooses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
|
||
|
|
7c69e0de08 |
[feat](trx-rs): add --check-config to the server and client
Validating a config meant starting the daemon and reading the first error it
died on, fixing that, and repeating. Add --check-config, which loads the
config through the real loader, reports every problem at once and exits 0/1:
$ trx-server --check-config --config trx-rs.toml
trx-rs.toml
warning: unknown config key 'listen.prot' (did you mean 'listen.port'?)
error: [general].log_level 'verbose' is invalid (expected one of: ...)
error: [rig.access].baud must be > 0 for serial access
error: [audio].frame_duration_ms must be one of: 3, 5, 10, 20, 40, 60
error: [listen] and rig "default" [audio] would both bind 127.0.0.1:4530
Validation grows validate_all()/validate_resolved_all() alongside the existing
first-error entry points; validate() is now the first element of validate_all().
Sockets are built by one helper shared by startup and the check, so the two
cannot disagree about what --listen overrides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
|
||
|
|
fbc4f6e398 |
[feat](trx-config): add a resolved-config validation phase
Some things can only be checked once CLI overrides have been folded in and the rig/remote lists are final, so nothing checked them at all: - The client's per-rig maps (rigctl.rig_ports, audio.rig_urls, audio.rig_ports, decode_history_retention_min_by_rig, http.default_rig_name) are keyed by a remote's short name. A typo used to spawn a rigctl listener that injected a rig_id no remote answered to, without a word in the log. - Nothing noticed two listeners claiming one socket. [listen].port and a rig's [audio].port could both be 4530; on the client, http, http_json and each rigctl rig port could collide freely. Add validate_resolved() to both configs, run after argument parsing, plus a shared socket-conflict check that treats a wildcard address as conflicting with any address on the same port and ignores port 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
d42ca4f030 |
[fix](trx-config): validate every rig, not just the legacy flat one
ServerConfig::validate() checked the flat [rig]/[audio]/[behavior] fields and gave [[rigs]] entries only an id/audio-port uniqueness pass, and validate_sdr() returned early unless the *flat* access type was "sdr". A multi-rig SDR station therefore got no Nyquist, stream_opus, duplicate-decoder or tx_enabled checking at all, and a rig entry with frame_duration_ms = 7 or a missing baud rate started and failed at runtime. Move the per-rig rules into validate_rig_instance() and validate_sdr_instance() and run them over resolved_rigs(), which already synthesises the flat layout as a single entry. Both layouts now go through the same code, and multi-rig messages name the rig they came from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
335922fecc |
[feat](trx-config): report unknown configuration keys
Every config struct is #[serde(default)], so a misspelled key was dropped in
silence and the setting kept its default. Writing `prot = 9999` under
[listen] started the server on 4530 without a word.
Collect the ignored key paths with serde_ignored and pair each with the
closest known key at the same level:
WARN unknown config key 'listen.prot' (did you mean 'listen.port'?)
Warnings by default, so a config written for a newer version still runs on an
older binary; --strict-config makes them fatal for CI. Logging now starts
before validation so these warnings are actually visible.
trx-configurator --check drops its hand-maintained key lists and re-implemented
range checks in favour of the real loader and validators, so it no longer
passes configs the binaries reject.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
|
||
|
|
bede2e34fe |
[fix](trx-config): accept both sectioned and bare config files
trx-configurator wrote standalone configs with [general]/[rig] at the root
while the loader required a [trx-server] section header, so every config the
wizard generated with --type server or --type client was rejected by the
binary it was generated for:
$ trx-server --config trx-server.toml
Error: ParseError("trx-server.toml", "missing [trx-server] section")
Teach the loader to fall back to the document root when no section header is
present, so hand-written standalone files keep working, and have the wizard
emit the same sectioned shape --print-config does. A file carrying only the
*other* component's section still reports the missing section rather than
silently loading defaults.
Round-trip tests now load every document the wizard can generate through the
real loader and validator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
|
||
|
|
da58a004fe |
[refactor](trx-config): extract client/server config into a shared crate
The setup wizard, the server and the client each carried their own idea of what a valid config looks like: trx-configurator validated with hand-written toml_edit key lists while the binaries validated with serde plus their own validate(). Nothing kept the three in sync. Move ServerConfig, ClientConfig, the section loader, the shared validators and the endpoint-URL parsing into a new trx-config crate that all three depend on, so there is one definition of the config to drift from. The binaries keep a thin crate::config re-export so their internal paths are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
77b283cb78 |
[fix](trx-frontend-http): size the nav's labels against the screen, not the font
CI / lint (pull_request) Successful in 2m17s
CI / test (pull_request) Successful in 8m23s
CI / frontend (pull_request) Successful in 4m20s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m17s
CI / test (push) Successful in 7m35s
CI / frontend (push) Successful in 3m29s
CI / reuse (push) Successful in 2s
CI cut "Bookmarks" off at 360 px where this machine had ten pixels to spare. How wide a platform draws a word varies by more than ten per cent, so a fixed 0.6rem label is a bet on the machine it was measured on -- the same bet ui-core's own comment warns about where it explains why the tab strip reflows by measurement rather than at a width. The long labels are sized with `clamp(0.46rem, 2.2vw, 0.62rem)` now. A tab is a fifth of the viewport, so what fits in it follows the viewport; tying the label to the same thing leaves better than a fifth of the tab spare at every phone width, and holds with the text drawn 30% wider than it is here. The test asked the wrong question too. "Does the label fit" is a question about font metrics, and it will keep answering differently on different machines. It now asserts what actually matters: a label stays inside its own tab, and if it is too long for it, it ends in an ellipsis rather than being cut through a letter. Both hold whatever width the platform draws the words at. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
6284747339 |
[fix](trx-frontend-http): let the bottom nav's labels fit inside its tabs
The bottom nav keeps its labels under the icons -- that is what makes it navigation rather than five glyphs -- but the labels did not fit the tabs. On a 360 px screen "Bookmarks" and "Digital modes" were cut off mid-word and ran into each other: "Bookmarks igital mode". The stylesheet already meant to handle it. Three rules shrink the long labels, and a rule twenty lines further down sets the size for all of them; identical specificity, later in the file, so the blanket rule won and nothing was ever shortened. Those rules now come after it. Beyond that, tabs sized themselves to their own labels, so "Map" and "Digital modes" were given the same room. They divide the bar evenly now, which is the shape of every bottom nav, and a label that still runs long ellipsises rather than being cut through a letter. "Digital modes" does not fit at any size worth reading, so the nav shows "Digital" and the button carries the full name as its accessible label; the short form is hidden from assistive tech, which reads the button's name instead. The phone layout test now checks that no label in the nav is cut off at 430, 390 or 360 px, and that the shortened tab still says what it is to something that reads the page rather than looks at it. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
14ad6e241b |
[fix](trx-frontend-http): stop the radio controls running off the side of a phone
On a 390 px screen the transmit controls were laid out at x=400, off the side of a tray 354 px wide: present, invisible, and reachable only by a horizontal scroll with nothing to say it was there. The page scrolled sideways by a dozen pixels as well. Three causes, each in a different place. A container query at the end of the stylesheet re-imposes `flex-wrap: wrap` on a narrow tray's rows. That is right while a row runs left to right; below the phone breakpoint the row is a column, and wrapping a column starts a *second column* — which is what put the transmit controls beside the tray rather than under it. The rule outranks the phone one, two classes to its one, and sits later in the file, so it now excludes itself below that breakpoint rather than being overridden. The tray is a grid, and a grid column sizes to its content. One row wider than the screen — the mode picker, six buttons across — dragged the whole tray out with it. `minmax(0, 1fr)` lets the column be as narrow as the phone and the rows wrap inside it. The wavelength and signal-strength readouts are given the width of their column on narrow screens, but with padding on a content box that is the column's width plus the padding. Those dozen pixels were the page's sideways scroll. They are border-box now. Also: the rig picker was taking 139 px of a 338 px bar, pushing the rest of the top bar into the overflow menu. It is capped and ellipsised on phones, with the full name still in the menu it opens. The map's filter bar, collapsed, keeps both its anchors and shrinks inside them rather than dropping `left` to be sized by shrink-to-fit, and no longer asks for a compositing layer it does not need. An absolutely positioned, backdrop-filtered, composited box sizing itself from its content is the shape of thing that renders as nothing on an engine other than the one it was written against — which is what Edge does with it. Unverified there: this machine has no Edge to test with. tests/mobile-layout.mjs holds the page to it at 430, 390 and 360 px: no sideways scroll, nothing laid out past the right edge, the transmit controls inside the screen, the rig picker within its cap — and the collapsed filter bar still on screen with a button to bring the filters back. The SSTV panel test stamped its picture with a fixed date, which the panel's own retention window dropped once that date was a day old. It uses a recent stamp now. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
06971ff65c |
[style](trx-frontend-http): make the spectrum control strip one strip
The row of controls under the plot held four different control heights, units as loose text beside the field they belonged to, and a quarter of its width as a hole in the middle. Between about 1100 and 1400 px it came apart: the bandwidth cluster wrapped to two lines while the level cluster stayed on one, so the two sat at heights that matched neither each other nor anything else on the page. Every control stays, in its order, with its name and its behaviour. This is the styling and the layout. A field is now one box -- name, value and unit inside a single border -- so a number cannot be read apart from the unit it is in. Fields, buttons, the peak-hold select and the contrast slider are one height, border-box so a button's own border cannot add two pixels to it, and 2.4rem under a coarse pointer where a fingertip needs the room. The contrast readout holds a fixed, tabular slot, so the row no longer twitches between 1.0 and 0.9. The container wraps and a cluster does not: a cluster that will not fit drops whole to the next line and starts it left-aligned. The slack goes to a spacer rather than to `space-between`, which is what opened the hole. Two things this turned up. The select carries `status-input` for other layouts' sake, which drew a box inside the field's box. And the narrow -screen rules lived in a media query earlier in the file than the rules they override -- identical specificity, so the later one won and the phone layout had been overflowing sideways rather than stacking. The narrow rules now sit directly after what they override. The layout test measures the strip at three widths: one height across every control, no overflow, inside the plot, and clusters either sharing a line or each having one -- never one floating against the middle of the other. docs/Spectrum-Controls-Rework.md records what was wrong and what was deliberately left alone: the two different Autos, the settings that do not persist, the one-shot buttons, and Sweet-spot's silence while it retunes the SDR. Those are behaviour, and are for another day. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
18107ce07e |
[feat](trx-rs): receive SSTV pictures end to end
CI / lint (pull_request) Successful in 2m16s
CI / frontend (pull_request) Successful in 4m12s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m15s
CI / test (pull_request) Successful in 9m37s
CI / test (push) Successful in 7m36s
CI / frontend (push) Failing after 31s
CI / reuse (push) Successful in 3s
Wires the SSTV decoder into the stack, from the audio the server already has to a panel in the browser that shows the picture arriving. Server: a decoder task alongside the WEFAX one, running whenever the decoder is enabled and the rig is in a mode SSTV is sent in. A finished picture is written to the cache as a PNG and sent on as a message; the rows are sent as they decode, so a client can watch two minutes of Martin M1 fill in rather than waiting for it. Pictures join the decode history, are replayed to a client that connects later, and survive a restart. Protocol: SetSstvDecodeEnabled and ResetSstvDecoder, a sstv_decode _enabled flag in the rig state, two audio message types, and Sstv and SstvProgress on DecodedMessage. The history stores the message without its base64 payload -- the picture is already on disk, and a megabyte per entry is not what a history is for. Client: pictures land in their own history, and the PNG the server sent is written to the local cache so /sstv-images/ can serve it back. That endpoint and the WEFAX one now share their filename checks rather than each carrying a copy: no separators, no parent references, .png only. Web UI: an SSTV sub-tab beside WEFAX, with a live canvas the rows paint into at the line number they carry, a card for the last picture, and a filterable history with links to the files. Rows below the one arriving are grey rather than black -- not yet received is a different thing from received as black. A picture is not a spot, so neither pictures nor their progress updates reach the decode statistics; that exclusion list had grown by hand for LRPT and WEFAX and is now one named set. The decoder crate gains what the server needed to hand a picture on: to_png, to_png_base64 and save_png, with file names stamped in UTC so they sort. Panel behaviour is tested with the plugin runtime: rows painting at their own line numbers rather than in arrival order, a completed picture linked by file name alone with no server path in the page, a cut-off picture reported as partial, clearing, and the toggle following the rig state. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
a0b0c0ed81 |
[feat](trx-sstv): decode SSTV pictures
CI / test (pull_request) Successful in 7m40s
CI / lint (pull_request) Failing after 14m36s
CI / frontend (pull_request) Successful in 3m12s
CI / reuse (pull_request) Successful in 3s
CI / test (push) Successful in 7m32s
CI / frontend (push) Failing after 1m21s
CI / reuse (push) Successful in 2s
CI / lint (push) Successful in 2m16s
A new decoder crate covering the modes SSTV is actually sent in: Martin M1/M2, Scottie S1/S2/DX, Robot 36/72, PD50 through PD290, and Wraase SC2-180. The mode comes from the VIS header every transmission opens with, so nothing has to be told what is arriving. Modes are a table rather than code: a list of segments -- sync, gaps, and one scan per colour channel -- plus a colour model and a geometry. The decoder reads the offset of each scan straight off that list, which is what makes fifteen modes cost about as much as one, and a new mode a table entry. The segment lists are checked against the published line durations in a test, because both are transcribed by hand from the same specification and a digit wrong in one is unlikely to be wrong identically in the other. Signal path: band-pass over the SSTV band, Hilbert FIR, instantaneous frequency by phase difference, then a state machine that walks the transmission a line at a time. Each line is looked for where the mode says it should be and nudged into place by the sync pulse found near it -- two sound cards never agree exactly, and over the two minutes of a Martin M1 frame an uncorrected error of a few parts per million shears the picture visibly. Rows are emitted as they decode, so a picture can be watched arriving, which is most of the appeal of the mode. Four things this cost, each now the reason a piece of it is shaped the way it is: The per-sample frequency estimate ripples by ±95 Hz at 1200 Hz, where the Hilbert approximation is weakest, though its mean is exact. Pixels average over their own window and were always right; the VIS bits and the sync detector classify individual samples and were reading the ripple. Both now read short means. Pixels deliberately still do not, so edges stay where they are. Broadband noise cost the whole picture, not part of it: a phase-difference detector answers whatever is loudest, and there was no input filter. Hence the band-pass, which is what every real decoder does first. A sync search window shorter than a sync pulse rejected every pulse arriving late in it, for being short. The first line's sync search locked onto the VIS stop bit -- 30 ms at exactly the sync frequency, immediately before the picture starts. The header already says where the picture begins, so the first line no longer searches. Tests: nine modes are encoded from a test card and decoded back, compared pixel by pixel, alongside silence around the signal, a transmission cut off part way, two transmissions back to back, 20 dB of noise, and a transmitter clock 0.1% fast. The encoder that produces those signals reads the same table as the decoder, so a round trip tests the decoder and not the timings; the timings are held to the published line durations separately. Nothing is wired into the server or the web UI yet: this is the decoder alone. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
a2c630a92b |
[feat](trx-frontend-http): put the tuned frequency in the address bar
CI / frontend (pull_request) Successful in 4m3s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m16s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m11s
CI / test (push) Successful in 7m20s
CI / frontend (push) Successful in 3m10s
CI / reuse (push) Successful in 2s
A receiver spreads by being linked to, and there was nothing to link to: the routes carried the tab and nothing else, so "listen to this" could only ever mean a screenshot and a frequency typed out in a message. The query string now carries the dial -- rig, frequency, mode and bandwidth -- in both directions. Opening a link selects the rig, sets the mode, tunes, then applies the bandwidth: a mode change brings its own default bandwidth with it, so an explicit bw has to land after it. Frequencies are read the way someone writes them by hand (7074k, 14.074M) and written back as whole Hz, so what comes out of the address bar is the same link in canonical form. After that the address bar keeps up with the dial, which is what makes it copyable at any moment rather than only at load. It is rewritten with replaceState -- tuning is not navigation, and a swept dial would otherwise bury the back button. A link button in the top bar copies the current link; it folds into the overflow menu when the bar is tight. Applying a link changes the radio, so an rx session says so instead of failing control calls one at a time. A tab listening to a virtual channel leaves the address alone rather than publishing a frequency the rig is not on, and bw is skipped in both directions on rigs without filter control, which would only refuse it. The fixture pinned every state frame to 100 MHz plus jitter to keep frames distinct, so no test could observe tuning at all. The jitter moves to the S-meter and the fixture echoes set_freq/set_mode/ set_bandwidth, as it already did for squelch. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
09634eb851 |
[fix](trx-frontend-http): tidy up the map's filter bar
CI / lint (pull_request) Successful in 2m21s
CI / test (pull_request) Successful in 8m7s
CI / frontend (push) Successful in 2m57s
CI / reuse (push) Successful in 3s
CI / frontend (pull_request) Successful in 3m47s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m16s
CI / test (push) Successful in 7m19s
The bar explained itself in prose: "All bands visible by default" sat between the chips and the next group, taking width the bar could not spare and reading as a stray line of text. An "All" chip says the same thing in a chip's width and gives the selection somewhere to be undone. Band chips also came up dimmed at the very moment every band was on the map -- an empty selection is no filter at all, so nothing is dimmed until something is picked. The path toggles drop their "On"/"Off" suffix, which cost most of a row and only repeated what their own highlight already said; state moves to aria-pressed and the tooltip. The rest is alignment. The rule dividing the buttons from the filters is drawn on the button block's edge, and a centred block left it floating as a stub beside a two-row bar; stacked, it lay down the left of a block that sits underneath. The labels sat at their natural widths, so each row's first control started somewhere different, and the two pairs of phase buttons differed in width, so the groups after them missed each other by four pixels. One gutter for every label, one width for both pairs, and the search field moved last where it can take the room the fixed-width groups leave. The map layout test now covers the chips, the divider's height and the rows' shared start. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
90ab7781ad |
[fix](trx-frontend-http): stop the browser offering saved values for frequency
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m11s
CI / frontend (pull_request) Successful in 3m48s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m24s
CI / test (push) Successful in 7m23s
CI / frontend (push) Successful in 2m54s
CI / reuse (push) Successful in 3s
The tuned and centre frequency readouts are text inputs, so the browser keeps what has been typed into them and offers it back in a dropdown -- Edge does this out of the box, dropping stale frequencies from other sessions over the reading. Turn autofill off on both, along with autocorrect and spellcheck, which have no business near a number either. Fixes #39 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
88d04253ca |
[fix](trx-frontend-http): move the map's fullscreen and filter toggles into the bar
Fullscreen and Hide Filters floated in their own block over the map's top-right corner, separate from the filter bar they sit beside. Put them at the right-hand end of the bar, behind a separator. What made this awkward before is that Hide Filters cannot live inside the thing it hides, so the collapse now applies to the filters alone: the bar keeps its two controls and shrinks to them at the map's right edge, leaving the whole map visible and the way back one click away. Fixes #38 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
08005c5c07 |
[feat](trx-frontend-http): lay the map filters out as a bar across the top
The filters were a 30rem column parked in the bottom-right corner, covering a third of the map they filter. Lay them out horizontally instead: one row per group -- label beside its control, thin rules between -- across the top of the map, spanning ~87% of its width at 1600px and wrapping to a second row as it narrows. It starts clear of Leaflet's zoom buttons and stops short of the corner controls, which stay outside it: the button that hides the filters cannot live inside the thing it hides. The bottom-left band legend keeps its place. The sentence explaining the two path toggles would have swallowed the bar, so it moves to their tooltips and is shown inline only in the stacked narrow-screen layout. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
026f816ddb |
[fix](trx-frontend-http): replay stored decodes onto the map when it loads
The map module is lazy: it arrives when the Map tab is first opened, which is normally long after startup restored the decode history. Until then aprsMapAddStation, aisMapAddVessel and vdesMapAddPoint are undefined, and the decoders' `if (lat != null && ... && fn)` guards quietly dropped every restored position. Nothing replayed them once the module did arrive, so the map came up empty and filled in only from decodes heard afterwards -- a station heard once was never plotted at all. A second reload appeared to fix it because the cached module then loaded early enough to win the race against the history fetch. Give DecoderPlugin an optional syncMap(), implement it for APRS, AIS and VDES over the history each already retains, and have map-core call trxPluginRuntime.syncMapAll() as it attaches. The add functions are keyed by callsign, MMSI and point, so replaying updates in place and cannot duplicate a marker; the replay runs oldest-first so tracks are rebuilt in the order they happened. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
d31b6f545f |
[test](trx-frontend-http): watch the history progress from inside the page
The replay-progress check polled the overlay from the test every 100ms. A replay that starts and finishes between two polls is never sampled, and the test then reports that no progress was shown at all -- the source of the intermittent "no progress was shown while the history loaded" failure. Record the samples from a MutationObserver installed before the page's own scripts run, so a fast replay is observed rather than missed. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
39f551c914 |
[fix](trx-frontend-http): hold the APRS symbol column open for frames without one
renderLocalAprsSymbol() returns nothing when a packet carries no symbol table or code, so those rows lost the icon's 24px slot and every column after it -- callsign, type badge, summary -- slid left against the rows around them. Frames that do carry a symbol then read as indented. Render an empty slot of the same size instead, so a list mixing position reports with messages and telemetry still lines up. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
6d25ecdc11 |
[feat](trx-frontend-http): give the AIS list the same log shape
A message was three stacked lines — time and name, then MMSI and route, then motion, distance, position and age — so a screen held eight of them. It is one line now: time, vessel, message type, and what the message says, opening in place for the MMSI, the channel frequency, the route, the age, the fix and a jump to the map. Twenty-two fit where eight did. What a message says depends on what it is. Position reports give the fix and the motion; the static and voyage reports that carry no fix give the callsign and where the vessel is bound. Both fall back to whatever fields are present rather than showing nothing. The row vocabulary the APRS list introduced is no longer APRS-specific — the classes are decode-line and decode-expanded now, shared by both, and identity sits in fixed columns so the summaries line up down the list instead of starting wherever the callsign happens to end. The three summary cards above the list go the way of the APRS ones. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
37987b2779 |
[feat](trx-frontend-http): make the APRS list a log, and read the payloads
A frame was a card five lines tall — timestamp, a meta line, the information field as it arrived on the air, three buttons, and a Details panel repeating the four things already on the row — so five frames filled the panel and the payload was left to be decoded by eye. A frame is one line now: time, station, type, and what the frame says. It opens in place for the path, the CRC, the raw field, its bytes and the actions. Twenty-one frames fit where five did. And the information field is read rather than echoed. Weather reports give temperature, wind, humidity and pressure; telemetry gives its sequence and channels; a message gives its addressee and text; a position gives the fix, course and speed, and the comment the station wrote. Anything that cannot be summarised falls back to the raw field, which is in the expanded view either way. HF APRS had a copy of the same forty lines of markup, differing by one badge, so both now build their rows from one function in the shared module — the CSS is shared between them and this would have broken it otherwise. Both headers lose their three summary cards for a line of counts beside the filters, which frees another fifth of the panel. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
67a7bace4e |
[fix](trx-frontend-http): let the decode lists fill their panel
FT8, FT4, FT2 and WSPR size their list against the panel with flex, and the sidebar layout made the panel a grid item aligned to the start of its row — sized to its own content. The lists collapsed to their 120px minimum with several hundred pixels of the page empty underneath. The panel stretches to the row now and the sidebar keeps its own height. The marine lists were sized a different way, by formula: 100vh minus a guess at everything above them. That guess stopped matching the moment the panel changed shape, so they left a few hundred pixels unused as well. They fill the panel like the rest now, and so does CW, which had a 360px ceiling. HF APRS had no container styling at all — no scroller, no frame, no height — so its packets ran down the page. It gets what the other packet lists have. The smoke test measures each list against its panel and requires it to scroll on its own. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
b48cc23d6e |
[fix](trx-frontend-http): keep the rig names through the state stream
The picker and the header showed each rig's lowercase id instead of its configured name. applyRigList takes the names as a parameter defaulted to an empty map, and the state-update path passes only the rig ids — names come from /rigs, not from a state frame — so that call landed on the default and the body, which treats "an object" as "here are the names", cleared them. One frame after load the names were gone for the rest of the session. Omitted now means no news rather than no names. The fixture is why this was invisible: it pushed an identical status payload every tick and the client skips a frame equal to the last, so render never ran and neither did the call that did the damage. Its event stream varies between frames now, as a real one does. Which immediately caught a second fault: state frames arrive continuously, and one sent before the server applied a new squelch threshold snapped the line back to where it had just been dragged from. A local change outranks the echo for two seconds, the same idea as the optimistic frequency guard beside it. The fixture also records what /set_sdr_squelch sets and reports it back afterwards — the drag test had been passing against a server that ignored the write. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
c1899229a0 |
[fix](trx-frontend-http): stop the decode history replay giving up at 20s
Reloading a second time sometimes showed history the first load did not, and the safety valve is why: it called one function that both released the buffered live decodes and tore the history worker down, so any load where the replay had not finished inside twenty seconds — a large backlog, a cold cache, a slow link — dropped whatever had not arrived, without a word. A reload got another go at it, and the second one is faster because everything is cached by then. Those are two separate things now. At the timeout the live decodes are released so the panels are not held back, the replay carries on, and the progress says so. The fallback's error path retries once and then says "Decode history unavailable" rather than leaving the operator to guess whether there was anything to see. The progress is no longer a scrim. It was fixed to the whole viewport with a wash over the page — the waterfall, the decode panels, all of it — for the length of the replay, which is exactly when there is something worth watching. It is a corner card with a bar: indeterminate while the payload is on the wire, then filling as N of M messages replay. None of this was reachable from a test. /decode/history answers in CBOR and the worker reads the body as CBOR unconditionally, but the fixture served JSON, so every browser run had been exercising the client's retry path and never its history path. It encodes CBOR now, including the 64-bit form the millisecond timestamps need, and decode-flow serves 1200 records and holds the client to restoring all of them on the first load, showing progress while it does, and never covering the page with it. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
84a99a3636 |
[fix](trx-frontend-http): load the decoders that own the digital modes panels
AIS, VDES and both APRS decoders were listed under the map plugin group alone, so opening Digital modes and clicking AIS or APRS gave an empty panel reading "Connected, listening for packets" while the decodes piled up unprocessed in the plugin runtime. They appeared only if something had opened the Map tab first, which flushed the queue. There is also a map-data group naming exactly those four that nothing loads: the loader is called with tab names and no tab is called map-data. They load with the tab whose panels they fill now. map-core stays lazy, since their calls into it are optional and the Map tab can go on paying for Leaflet by itself. tests/decode-flow.mjs follows a decode from the wire to the map: an AIS vessel and an APRS beacon arrive on /decode, and it asserts both panels fill with the map module confirmed absent, the mini view names the vessel and offers a pin, following that pin lands on /map centred on the vessel, and both decoders leave a marker. Nothing exercised any of this before — the fixture served an empty decode stream, which is how the map links came to be broken for every decoder at once. The fixture stamps decodes as it sends them, since the client prunes anything outside the retention window, and repeats them, since the views collapse by vessel and need more than one frame to behave. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
84bdf2593c |
[fix](trx-frontend-http): measure whether the tab bar fits, and watch for it
CI put the tab strip 9px into the controls at 1440px with no scaling at all, on a bar that had every degradation step available to it and used none of them. It used none because nothing thought anything was wrong: the fit test was an arithmetic estimate — identity + nav.scrollWidth + actions.scrollWidth + a 48px allowance for the gaps — and on a platform whose fonts run wider than the one it was written on, that allowance no longer covered what it stands for. An estimate that says "fits" stops the ladder before its first rung. It reads the geometry now: the controls have to stay inside the bar, and no tab may reach them. That is the same measurement the test makes, so the two cannot disagree about any platform's metrics. The tabs are the subject rather than the nav's box because the nav shrinks below its content — the box gets smaller while the tabs keep their width and slide underneath the controls. A second fault turned up while probing this: the strip only reflowed on window resize. The rig name arriving from the server, the style picker filling in, a font swapping in wider metrics — each changes what fits without touching the window, and the bar sat there as it was through all of them. A ResizeObserver on the bar and the controls covers those, and document.fonts.ready covers the swap. The guard sweeps text scales and adds a station name too long for the bar, but it should be said plainly: it passes against the old code too. Nothing here reproduces on this machine — a 4px viewport sweep from 1080 to 1500, three wide font stacks and scales from 1.0 to 3.0 all failed to make the old estimate lie. What is fixed is the mechanism that could. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
c527f20a88 |
[feat](trx-frontend-http): give digital modes a decoder sidebar
Thirteen decoders in a horizontal strip needed a scroller on anything but a wide window, and the open one was marked by a single underline among thirteen. They are a list down the left now, all visible at once, each keeping the state dot it already carried, with the panel for the selected one filling the rest of the width. No script changed: the sub-tab wiring, the aria roles, the decoder picker and the state-dot observers all work on the same markup, so this is layout only. Below 760px the sidebar gives way to the picker that already existed there. That path needed align-content: the tab panel fills the page height and a grid stretches its rows to match, which handed the picker a 218px row and left a 189px gap under it. The tab icon was the signal-strength bars, which is what the S-meter shows two rows above it; a pulse train says digital modes instead. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
9f495021f0 |
[style](trx-frontend-http): name and fence the three audio groups
The row carries three unrelated things — how loud it is, whether there is any audio at all, and how much is arriving — and only the middle one was named. Each is a group now: VOLUME over the two sliders, SQL on its own switch, LEVEL over the meter, with a hairline between each. The rules are drawn only while the row is one line, measured against the row rather than the viewport: what fits depends on whether the rig transmits and whether it has a squelch at all. A rule divides what sits either side of it, so once a group wraps the wrap is the division and the rule would just be a mark at the start of a line — which is what it was at 900px before this. The labels carry the grouping on their own below that width, and the groups stack whole on a phone. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
33aa7b807c |
[style](trx-frontend-http): fence the squelch off from the volume controls
The squelch sat in the audio row on the same gap as everything else, so it read as a continuation of the volume sliders. It decides whether there is audio at all, which is not the same kind of control as how loud it is, and a hairline says so. The rule belongs to the squelch block, so it leaves with it on a rig that has none, and it stands down where the row stacks: there the line break separates them already and a leading rule would just start a line. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
2c56d82a81 |
[feat](trx-frontend-http): make the SQL label the squelch switch
Clicking SQL turns the squelch on and off. The label and the button beside it said the same thing twice — one naming the control, the other reading "On" or "Off" — where the name itself is the obvious target, and the dot already carries the state: grey when off, green while the gate passes, amber while it holds. The pressed state is on the label, so the switch reads the same to a screen reader as it looks. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
0fc2115973 |
[fix](trx-frontend-http): make map links work before the map has loaded
The AIS and APRS mini views link each position to the map, and neither did anything: the map module installs itself lazily, and it was the one defining window.navigateToAprsMap, so until something had opened the Map tab the global did not exist. AIS calls it inline from onclick and threw "not a function"; APRS guards the call and so failed silently. The grid links on FT8, FT4, FT2 and WSPR rows went the same way through navigateToMapLocator. The app owns both globals now, installed at startup. They record the target, switch tabs through navigateToTab — the only path that materialises the panel from its template, loads the module and updates the history entry, none of which the module's own hand-rolled tab switch did — and the target is applied once the module reports ready. The module keeps the focusing, which is its job, and exposes it as focusMapPosition and focusMapLocator. The smoke test now calls the link from a cold page, asserting the map module is not loaded first so the check cannot pass by accident. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
2c1df75d19 |
[fix](trx-frontend-http): measure auto squelch from the meter
Auto took the spectrum's noise floor and added 6 dB, but the threshold is compared against the channel level the meter reports, and the two sit a long way apart: the gap is set by the FFT size and window, the channel bandwidth, the decimation, and peak-versus-mean statistics. Measured on white noise it runs +22.1 dB at 48k/8k/3k, +18.7 dB at 240k/24k/12k and -1.2 dB at 1.92M/24k/12k — a 23 dB swing across ordinary configurations. Only the last of those is anywhere near right, so on a narrow span Auto set the gate some 20 dB below the noise and it never closed. It now reads the same number the DSP compares: the 20th percentile of the meter over the last ten seconds, plus 5 dB. The percentile keeps a burst of traffic inside the window from dragging the estimate up, and 5 dB clears the meter's own jitter, which measured 0.9-1.6 dB. Nothing in it converts between scales, so no part of the signal chain can put it out again. With no history yet — a fresh connection, a rig switch — it listens for a moment rather than refusing. The fixture gained a streaming /meter, without which there is nothing to measure, and the spectrum test pins auto to the meter it serves. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
aefd36c4b1 |
[feat](trx-frontend-http): set the squelch on the spectrum, in dB
The threshold is in dB, and since the squelch fix that is the scale the spectrum axis and the S-meter are labelled in — so the control belongs on the plot, at the level it gates. A dashed line spans the spectrum at its threshold with a grip that reads it out, dragged like the bandwidth edges, green while the signal is above it and amber while it gates. Arrow keys move it a dB at a time for anyone not using a mouse. The audio row keeps a compact version: the dB, an indicator lit from the same meter the DSP compares against, Auto, and an enable toggle that no longer doubles as the level. The slider ran 0-100% over that dB range, which gave the operator a number with nothing on screen to relate it to, and zero meant "disabled", so turning the squelch off to listen threw the threshold away. Auto now says which level it picked. Two things the browser could only show once it was on the plot: the grip landed underneath the split control at the right edge, which swallowed its pointer, and dragging to the foot of the axis hid the line — and the grip with it — instead of pinning it where it could be dragged back. The fixture could not exercise any of this: /audio answered 404, which hides the audio row and the control inside it, and the status carried no filter block, which is what tells the client the rig has a squelch at all. Both now look like an SDR, and the spectrum test drives the line. Signed-off-by: Stan Grams <sjg@haxx.space> |