Ten fixes to how client and server configuration is defined, loaded, validated
and documented. Each is a separate commit and can be read on its own.
The bugs
The setup wizard produced configs no binary could load.trx-configurator --type server wrote [general]/[rig] at the document root while the loader
demanded a [trx-server] section header:
The loader now accepts both shapes, the wizard emits the sectioned one, and
round-trip tests push every document it can generate through the real loader.
Misspelled keys were dropped in silence. Every config struct is #[serde(default)], so prot = 9999 under [listen] started the server on
4530 without a word. Ignored keys are now collected and reported with a
suggestion; --strict-config makes them fatal.
[[rigs]] entries were barely validated.validate() checked only the flat
legacy fields, and validate_sdr() returned early unless the flat access type
was "sdr" — so a multi-rig SDR station got no Nyquist, stream_opus,
duplicate-decoder or tx_enabled checking at all.
Nothing checked per-rig map keys or port collisions.rigctl.rig_ports and
friends are keyed by remote short name; a typo spawned a listener routing to a
rig that did not exist. [listen].port and a rig's [audio].port could both be
4530.
The additions
--check-config on both binaries: reports every problem at once, exits 0/1.
[decoders] per rig: nine decoders used to start unconditionally per rig
whether or not anyone looked at the results. enabled defaults to all of
them, so upgrading changes nothing; output_dir replaces the hard-coded
cache paths for SSTV/WEFAX/LRPT.
Secrets outside the config: ${VAR} expansion (an unset variable is an
error, not a silently blank passphrase) and a *_file sibling for every
credential, plus a warning when a config holding secrets is group-readable.
Deprecation warnings for keys that stopped doing what they look like they
do, keyed off which paths the file actually set so defaults never warn.
Structural change
ServerConfig, ClientConfig, the loader and the validators move into a new trx-config crate shared by the server, client and wizard. The wizard used to
carry a second implementation — hand-maintained lists of known keys plus
re-implemented range checks — which passed configs the binaries reject; it now
defers entirely to the real code. trx-rs.toml.example is generated from the
structs with a test that fails when the checked-in copy drifts.
Documentation
The manual was wrong about the basics: it listed five config search paths, none
of which the loader has ever consulted, called --print-config output "fully
commented" when it carries no comments, and documented a TRX_PLUGIN_DIRS
variable no code reads. Corrected, along with the client tables that still
described [frontends.rigctl].port as the bind port years after rig_ports
replaced it.
Verification
863 tests pass, clippy is clean across the workspace, and the release build
succeeds. Behaviour changes were checked against the running binaries: the
typo warning, both file shapes, port-collision detection, decoder gating
(9 decoders by default, 2 with enabled = ["cw", "ft8"]), env expansion, *_file secrets, the permission warning, and every deprecation message.
Ten fixes to how client and server configuration is defined, loaded, validated
and documented. Each is a separate commit and can be read on its own.
## The bugs
**The setup wizard produced configs no binary could load.** `trx-configurator
--type server` wrote `[general]`/`[rig]` at the document root while the loader
demanded a `[trx-server]` section header:
```
$ trx-server --config trx-server.toml
Error: ParseError("trx-server.toml", "missing [trx-server] section")
```
The loader now accepts both shapes, the wizard emits the sectioned one, and
round-trip tests push every document it can generate through the real loader.
**Misspelled keys were dropped in silence.** Every config struct is
`#[serde(default)]`, so `prot = 9999` under `[listen]` started the server on
4530 without a word. Ignored keys are now collected and reported with a
suggestion; `--strict-config` makes them fatal.
**`[[rigs]]` entries were barely validated.** `validate()` checked only the flat
legacy fields, and `validate_sdr()` returned early unless the *flat* access type
was `"sdr"` — so a multi-rig SDR station got no Nyquist, `stream_opus`,
duplicate-decoder or `tx_enabled` checking at all.
**Nothing checked per-rig map keys or port collisions.** `rigctl.rig_ports` and
friends are keyed by remote short name; a typo spawned a listener routing to a
rig that did not exist. `[listen].port` and a rig's `[audio].port` could both be
4530.
## The additions
- **`--check-config`** on both binaries: reports every problem at once, exits 0/1.
- **`[decoders]` per rig**: nine decoders used to start unconditionally per rig
whether or not anyone looked at the results. `enabled` defaults to all of
them, so upgrading changes nothing; `output_dir` replaces the hard-coded
cache paths for SSTV/WEFAX/LRPT.
- **Secrets outside the config**: `${VAR}` expansion (an unset variable is an
error, not a silently blank passphrase) and a `*_file` sibling for every
credential, plus a warning when a config holding secrets is group-readable.
- **Deprecation warnings** for keys that stopped doing what they look like they
do, keyed off which paths the file actually set so defaults never warn.
## Structural change
`ServerConfig`, `ClientConfig`, the loader and the validators move into a new
`trx-config` crate shared by the server, client and wizard. The wizard used to
carry a second implementation — hand-maintained lists of known keys plus
re-implemented range checks — which passed configs the binaries reject; it now
defers entirely to the real code. `trx-rs.toml.example` is generated from the
structs with a test that fails when the checked-in copy drifts.
## Documentation
The manual was wrong about the basics: it listed five config search paths, none
of which the loader has ever consulted, called `--print-config` output "fully
commented" when it carries no comments, and documented a `TRX_PLUGIN_DIRS`
variable no code reads. Corrected, along with the client tables that still
described `[frontends.rigctl].port` as the bind port years after `rig_ports`
replaced it.
## Verification
863 tests pass, clippy is clean across the workspace, and the release build
succeeds. Behaviour changes were checked against the running binaries: the
typo warning, both file shapes, port-collision detection, decoder gating
(9 decoders by default, 2 with `enabled = ["cw", "ft8"]`), env expansion,
`*_file` secrets, the permission warning, and every deprecation message.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
sjg
merged commit 46c9827e8a into main2026-08-06 22:22:40 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Ten fixes to how client and server configuration is defined, loaded, validated
and documented. Each is a separate commit and can be read on its own.
The bugs
The setup wizard produced configs no binary could load.
trx-configurator --type serverwrote[general]/[rig]at the document root while the loaderdemanded a
[trx-server]section header:The loader now accepts both shapes, the wizard emits the sectioned one, and
round-trip tests push every document it can generate through the real loader.
Misspelled keys were dropped in silence. Every config struct is
#[serde(default)], soprot = 9999under[listen]started the server on4530 without a word. Ignored keys are now collected and reported with a
suggestion;
--strict-configmakes them fatal.[[rigs]]entries were barely validated.validate()checked only the flatlegacy fields, and
validate_sdr()returned early unless the flat access typewas
"sdr"— so a multi-rig SDR station got no Nyquist,stream_opus,duplicate-decoder or
tx_enabledchecking at all.Nothing checked per-rig map keys or port collisions.
rigctl.rig_portsandfriends are keyed by remote short name; a typo spawned a listener routing to a
rig that did not exist.
[listen].portand a rig's[audio].portcould both be4530.
The additions
--check-configon both binaries: reports every problem at once, exits 0/1.[decoders]per rig: nine decoders used to start unconditionally per rigwhether or not anyone looked at the results.
enableddefaults to all ofthem, so upgrading changes nothing;
output_dirreplaces the hard-codedcache paths for SSTV/WEFAX/LRPT.
${VAR}expansion (an unset variable is anerror, not a silently blank passphrase) and a
*_filesibling for everycredential, plus a warning when a config holding secrets is group-readable.
do, keyed off which paths the file actually set so defaults never warn.
Structural change
ServerConfig,ClientConfig, the loader and the validators move into a newtrx-configcrate shared by the server, client and wizard. The wizard used tocarry a second implementation — hand-maintained lists of known keys plus
re-implemented range checks — which passed configs the binaries reject; it now
defers entirely to the real code.
trx-rs.toml.exampleis generated from thestructs with a test that fails when the checked-in copy drifts.
Documentation
The manual was wrong about the basics: it listed five config search paths, none
of which the loader has ever consulted, called
--print-configoutput "fullycommented" when it carries no comments, and documented a
TRX_PLUGIN_DIRSvariable no code reads. Corrected, along with the client tables that still
described
[frontends.rigctl].portas the bind port years afterrig_portsreplaced it.
Verification
863 tests pass, clippy is clean across the workspace, and the release build
succeeds. Behaviour changes were checked against the running binaries: the
typo warning, both file shapes, port-collision detection, decoder gating
(9 decoders by default, 2 with
enabled = ["cw", "ft8"]), env expansion,*_filesecrets, the permission warning, and every deprecation message.🤖 Generated with Claude Code
https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
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>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>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>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>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>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>