feat(kiwisdr): honest, API-policy-aware public-receiver browser#3679
Conversation
Adds KiwiPublicDirectory: an honest reader of the public KiwiSDR directory (kiwisdr.com/public) that surfaces each operator's external-API policy so AetherSDR honors "web-only" receivers before ever attempting a connection. - Parses the directory's per-receiver <!-- key=value --> metadata, exposing ext_api as KiwiPublicReceiver::apiPolicy() + mayConnectViaApi() (ext_api>0). ext_api=0 means the operator disabled the external API (web-only). - fetch() does the honest interactive flow: gate -> token-unlock -> list, with an "AetherSDR/<ver>" User-Agent (never a spoofed browser), auto-skipping the token dance when the IP is already past the gate. Strictly manual — no polling, caching, or enumeration. - tools/kiwi_directory_poc.cpp: demonstration tool (live fetch or offline parse) printing the per-operator policy breakdown + the honor decision per receiver — the proof-of-concept to show operators. - tests/kiwi_public_directory_test.cpp: locks the parser + policy logic (web-only receiver must never be API-connectable; picker filter excludes it). - docs/kiwisdr-public-directory.md: the good-citizen contract. Reads only server-published data (directory HTML + /status), never the KiwiSDR source (clean-room, Principle IV). Live run: 824 receivers, 121 web-only correctly excluded from API use. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds "Browse public…" to the KiwiSDR add-antenna row (Settings → Antennas). It opens KiwiPublicReceiverPicker, which fetches the public directory on open (one explicit user action) and lists ONLY receivers whose operator allows the external API. Receivers with ext_api=0 are filtered out entirely and never shown — AetherSDR honors "web-only" operators by not offering them. The status line reports how many web-only receivers were hidden. Picking a receiver fills the endpoint (host:port) and a suggested name into the new-profile row so the user can confirm/rename before committing; the existing addProfile flow is unchanged. Search filters by name/location/host. Built on the tested KiwiPublicDirectory reader (honest UA, manual-only fetch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Don't hammer operators' servers: the public directory is fetched only on the first "Browse public…" of a session. The result is held in a process-static cache shared by every picker instance, so subsequent opens re-serve it with no network request. The cache is in-memory only — never written to disk — so a new app session always starts fresh. The "Refresh" button is renamed "Refresh list" and is the sole way to force a re-fetch (it overwrites the cache). The status line shows when the shown list is cached. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Selecting a receiver in the picker filled the name/endpoint fields but didn't commit them — the profile was only added once a field's editingFinished fired, so the user had to Tab out to actually add it (bad discoverability). Reorder so commitNewRow() is defined before the Browse button and call it directly after the picker fills the fields, so picking a receiver adds the profile on the spot. Manual entry (editingFinished/returnPressed) is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks for this, @ten9876 — this is a well-constructed feature. The "honor ext_api before we ever connect" framing is genuinely thoughtful, the clean-room sourcing is clearly documented, and the code follows AetherSDR conventions nicely: PersistentDialog (AppSettings-backed geometry, not raw QSettings), RAII via Qt parent ownership, namespaced, and a real unit test that locks the parser + the three ext_api regimes. Network errors are handled at every step of the fetch and the reply lambdas are bound to this as context, so in-flight requests disconnect safely on dialog teardown. CI is fully green. A few suggestions, none blocking:
1. Hardcoded/stale User-Agent version. KiwiPublicDirectory::userAgent() returns "AetherSDR/26.6 …" but the project is at 26.6.3 (project(AetherSDR VERSION 26.6.3) in CMakeLists.txt), and it'll drift on every bump. There's already an AETHERSDR_VERSION compile definition — but note it's currently set PRIVATE only on the AetherSDR target (CMakeLists.txt:1661), so it won't be visible to the new kiwi_public_directory_test / kiwi_directory_poc targets that compile KiwiPublicDirectory.cpp directly. Suggest adding AETHERSDR_VERSION to those two targets' target_compile_definitions and using it here with a literal fallback, so the honest identity stays honest after the next release.
2. No transfer timeout on the fetch. The 3-step gate→unlock→list flow has no timeout, so if a server stalls mid-handshake the picker sits on "Loading public receivers…" indefinitely with only Cancel as an escape. A req.setTransferTimeout(...) on each QNetworkRequest (and surfacing it through failed()) would close that boundary cleanly.
3. http:// for the directory. kDirectoryUrl is plain HTTP. For a feature whose whole premise is being a good, honest network citizen, preferring https://kiwisdr.com/public/ (with HTTP fallback if needed) avoids the cleartext fetch.
4. Minor: "web-only hidden" count under-reports. In onReady, m_hiddenWebOnly only increments for ApiPolicy::Disabled. Receivers with an unpublished policy (extApi == -1 → Unknown) also fail mayConnectViaApi() and are dropped from the list, but aren't counted anywhere, so the status line's hidden total can be short. The docs say ext_api is always published so this may be rare in practice — but a small "+N unknown" in the status would keep the accounting honest.
One scope note (not a change request): this PR honors the policy at the browse layer — a user can still hand-type an ext_api=0 host into the endpoint field. The connection-time enforcement appears to live in #3678's /status preflight, so the two landing together is what makes the "never connect to a web-only receiver" guarantee complete end-to-end.
Nice work overall.
🤖 aethersdr-agent · cost: $3.0933 · model: claude-opus-4-8
…icy hidden (#3679 review) Review follow-ups (bot notes 1, 2, 4): 1. KiwiPublicDirectory::userAgent() now uses the build's AETHERSDR_VERSION instead of a hardcoded "26.6" that would drift each release; the test and PoC targets get the AETHERSDR_VERSION compile def so the honest identity is accurate everywhere (now "AetherSDR/26.6.3"). 2. Each of the 3 fetch requests (gate / unlock / list) gets a 15s transfer timeout, so a stalled server surfaces via failed() instead of leaving the picker stuck on "Loading…" with only Cancel. 4. The picker now counts receivers dropped for an unpublished policy (ApiPolicy::Unknown), not just web-only (Disabled), and surfaces both in the status line ("N web-only, M policy-unknown hidden") so the accounting is honest. Note 3 (prefer https for the directory) is not applicable — kiwisdr.com serves the directory over http only (port 443 refuses), so https would break the fetch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the review notes in c6e7d96: (1) the User-Agent now comes from the build's On note (3) — preferring |
…3750 H1) (#3751) Fixes the **H1** item from #3750 (SmartMTR fast-follow), introduced with the SmartMTR meter view in #3723. ## Problem `VfoWidget::pushSmartMtrInput()` hardcodes the FLEX dBm scale (`-127` S0 .. `-13` S9+60) for **every** source. When a KiwiSDR slice has no calibrated meter, the receive path sets the sentinel `m_signalDbm = -130` and the dBm label correctly shows **"Meter ---"** — but the SmartMTR needle clamped `-130` to S0 and rendered a confident **S0**, and the numeric value labels printed the bogus sub-S0 dBm. The PR author wasn't aware of our KiwiSDR public-receiver feed (#3679), so this interaction was on us (maintainers) to fix. ## Fix Track whether the current signal reading is a real calibrated dBm: - new `m_signalHasDbm` — `true` for FLEX (`setSignalLevel`), set from the KiwiSDR meter capability in `setReceiveMeterReading`; - feed it into `MeterInput::hasValue` for the `Signal` kind. The widget **already** parks/fades the indicator and suppresses the value labels when `hasValue == false` (see `indicatorPosition()` → `kScaleMin`, the `kSignalFadeLoDbm` needle target, and the `if (m_input.hasValue)` value-label guard). So no widget change is needed — the SmartMTR view now shows no-data, matching the "Meter ---" label, instead of a false S0. The mic (TX) path keeps `hasValue = true`. ## Scope 3 small edits in `VfoWidget.{cpp,h}`, behavior-neutral for FLEX (the flag is always `true` there). No change to the S-meter path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Adds a "Browse public…" picker to the KiwiSDR add-antenna flow (Settings → Antennas) that lists public KiwiSDR receivers from
kiwisdr.com/public— built from the ground up to honor each operator's external-API policy and to be a good network citizen.The core idea: a KiwiSDR's
ext_apivalue (published per-receiver in the directory and in/status) is the operator's external-API allowance. AetherSDR reads it and respects it before ever connecting.How it honors operators
ext_api=0are never shown and never connected to — AetherSDR (an API client) simply doesn't offer them. The picker's status line reports how many were hidden.AetherSDR/<ver>User-Agent; never spoofs a browser to pass the directory's interactive gate. If an operator blocks us, that's their answer./status), never the KiwiSDR (GPL) source.What's included
src/core/KiwiPublicDirectory.{h,cpp}— honest 3-step fetch (gate → token-unlock → list, auto-skips when the IP's already through) + parser; exposesapiPolicy()/mayConnectViaApi().src/gui/KiwiPublicReceiverPicker.{h,cpp}— frameless (PersistentDialog) picker, API-only list, search, session cache, "Refresh list"; picking a receiver adds the profile immediately.tools/kiwi_directory_poc.cpp(kiwi_directory_poc) — demonstration tool (live or offline) printing the per-operator policy breakdown + honor decision. This is the PoC shown to the KiwiSDR project.tests/kiwi_public_directory_test.cpp— locks the parser + honor logic (a web-only receiver must never be API-connectable and is excluded by the picker filter).docs/kiwisdr-public-directory.md— the good-citizen contract.Verification
ext_api=0) correctly excluded from API use, identifying honestly as AetherSDR.🤖 Generated with Claude Code