Fix and harden client/server configuration #48

Merged
sjg merged 13 commits from feat/config-improvements into main 2026-08-06 22:22:40 +02:00
13 Commits
Author SHA1 Message Date
sjgandClaude Opus 5 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>
2026-08-06 22:08:06 +02:00
sjgandClaude Opus 5 0fc977f19f [style](trx-config): apply rustfmt
CI / test (pull_request) Failing after 6m0s
CI / lint (pull_request) Successful in 2m18s
CI / frontend (pull_request) Successful in 3m28s
CI / reuse (pull_request) Successful in 2s
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>
2026-08-06 21:44:17 +02:00
sjgandClaude Opus 5 084f629b5b [docs](trx-rs): record the trx-config crate and its commands
CI / lint (pull_request) Failing after 1s
CI / test (pull_request) Failing after 6m6s
CI / reuse (pull_request) Has been cancelled
CI / frontend (pull_request) Has been cancelled
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>
2026-08-06 21:39:09 +02:00
sjgandClaude Opus 5 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>
2026-08-06 21:35:52 +02:00
sjgandClaude Opus 5 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>
2026-08-06 21:32:46 +02:00
sjgandClaude Opus 5 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>
2026-08-06 21:27:18 +02:00
sjgandClaude Opus 5 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>
2026-08-06 21:22:03 +02:00
sjgandClaude Opus 5 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>
2026-08-06 21:08:20 +02:00
sjgandClaude Opus 5 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>
2026-08-06 21:02:26 +02:00
sjgandClaude Opus 5 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>
2026-08-06 20:57:59 +02:00
sjgandClaude Opus 5 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>
2026-08-06 20:55:01 +02:00
sjgandClaude Opus 5 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>
2026-08-06 20:46:59 +02:00
sjgandClaude Opus 5 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>
2026-08-06 20:44:42 +02:00