Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,41 @@ jobs:
- name: It is actually universal
run: |
set -euo pipefail
# ⚠️ THE BINARY IS INSIDE AN .app ON macOS. qt_add_executable produces a bundle,
# so client/build/hamdeck-qml does not exist - the tools report "no such file",
# which reads as a build that produced nothing rather than a path that is wrong.
BIN=client/build/hamdeck-qml.app/Contents/MacOS/hamdeck-qml
[ -f "$BIN" ] || BIN=client/build/hamdeck-qml
# ⚠️ THE BINARY IS INSIDE AN .app ON macOS, UNDER THE DISPLAY NAME. The bundle
# is "HamDeck Remote.app" and the executable inside carries the same name, not
# the CMake target name - so every path here has a SPACE in it and must stay
# quoted. An unquoted one splits into "client/build/HamDeck" and reports "no
# such file", which reads as a build that produced nothing.
BIN="client/build/HamDeck Remote.app/Contents/MacOS/HamDeck Remote"
# ⚠️ NO FALLBACK TO A BARE client/build/hamdeck-qml. There used to be one, and
# it would now hide the thing most likely to break: if OUTPUT_NAME stops
# applying, the bundle is called hamdeck-qml.app again and a fallback would
# quietly build, test and ship the badly-named app that this rename exists to
# prevent. On APPLE the output is always a bundle, so a miss is a real fault.
[ -f "$BIN" ] || { echo "no binary at $BIN - the bundle is not named as expected"; ls client/build; exit 1; }
echo "architectures: $(lipo -archs "$BIN")"
lipo -archs "$BIN" | grep -q arm64 || { echo "missing arm64"; exit 1; }
lipo -archs "$BIN" | grep -q x86_64 || { echo "missing x86_64"; exit 1; }
# ⚠️ A BUILD IS NOT A BUNDLE. Everything above proves the binary is universal and
# runs; none of it looks at what macOS actually shows the operator. 0.1.29 passed
# every one of those checks and shipped an app Finder called "hamdeck-qml" with the
# blank generic icon, plus no microphone usage string - which SIGKILLs the app on
# the first PTT. This is the check that was missing.
- name: Is it a properly formed Mac application
run: python3 tools/check_macos_bundle.py "client/build/HamDeck Remote.app"

- name: Run it
run: |
# ⚠️ THE BINARY IS INSIDE AN .app ON macOS. qt_add_executable produces a bundle,
# so client/build/hamdeck-qml does not exist - the tools report "no such file",
# which reads as a build that produced nothing rather than a path that is wrong.
BIN=client/build/hamdeck-qml.app/Contents/MacOS/hamdeck-qml
[ -f "$BIN" ] || BIN=client/build/hamdeck-qml
# ⚠️ THE BINARY IS INSIDE AN .app ON macOS, UNDER THE DISPLAY NAME. The bundle
# is "HamDeck Remote.app" and the executable inside carries the same name, not
# the CMake target name - so every path here has a SPACE in it and must stay
# quoted. An unquoted one splits into "client/build/HamDeck" and reports "no
# such file", which reads as a build that produced nothing.
BIN="client/build/HamDeck Remote.app/Contents/MacOS/HamDeck Remote"
# ⚠️ NO FALLBACK TO A BARE client/build/hamdeck-qml. There used to be one, and
# it would now hide the thing most likely to break: if OUTPUT_NAME stops
# applying, the bundle is called hamdeck-qml.app again and a fallback would
# quietly build, test and ship the badly-named app that this rename exists to
# prevent. On APPLE the output is always a bundle, so a miss is a real fault.
[ -f "$BIN" ] || { echo "no binary at $BIN - the bundle is not named as expected"; ls client/build; exit 1; }
QT_QPA_PLATFORM=offscreen "$BIN" --selftest
51 changes: 40 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -553,11 +553,18 @@ jobs:

- name: Run the binary
run: |
# ⚠️ THE BINARY IS INSIDE AN .app ON macOS. qt_add_executable produces a bundle,
# so client/build/hamdeck-qml does not exist - the tools report "no such file",
# which reads as a build that produced nothing rather than a path that is wrong.
BIN=client/build/hamdeck-qml.app/Contents/MacOS/hamdeck-qml
[ -f "$BIN" ] || BIN=client/build/hamdeck-qml
# ⚠️ THE BINARY IS INSIDE AN .app ON macOS, UNDER THE DISPLAY NAME. The bundle
# is "HamDeck Remote.app" and the executable inside carries the same name, not
# the CMake target name - so every path here has a SPACE in it and must stay
# quoted. An unquoted one splits into "client/build/HamDeck" and reports "no
# such file", which reads as a build that produced nothing.
BIN="client/build/HamDeck Remote.app/Contents/MacOS/HamDeck Remote"
# ⚠️ NO FALLBACK TO A BARE client/build/hamdeck-qml. There used to be one, and
# it would now hide the thing most likely to break: if OUTPUT_NAME stops
# applying, the bundle is called hamdeck-qml.app again and a fallback would
# quietly build, test and ship the badly-named app that this rename exists to
# prevent. On APPLE the output is always a bundle, so a miss is a real fault.
[ -f "$BIN" ] || { echo "no binary at $BIN - the bundle is not named as expected"; ls client/build; exit 1; }
QT_QPA_PLATFORM=offscreen "$BIN" --selftest

# ⚠️ ASK THE BINARY, NOT THE FLAG. CMAKE_OSX_ARCHITECTURES is a request, and if Qt
Expand All @@ -567,15 +574,30 @@ jobs:
- name: It is actually universal
run: |
set -euo pipefail
# ⚠️ THE BINARY IS INSIDE AN .app ON macOS. qt_add_executable produces a bundle,
# so client/build/hamdeck-qml does not exist - the tools report "no such file",
# which reads as a build that produced nothing rather than a path that is wrong.
BIN=client/build/hamdeck-qml.app/Contents/MacOS/hamdeck-qml
[ -f "$BIN" ] || BIN=client/build/hamdeck-qml
# ⚠️ THE BINARY IS INSIDE AN .app ON macOS, UNDER THE DISPLAY NAME. The bundle
# is "HamDeck Remote.app" and the executable inside carries the same name, not
# the CMake target name - so every path here has a SPACE in it and must stay
# quoted. An unquoted one splits into "client/build/HamDeck" and reports "no
# such file", which reads as a build that produced nothing.
BIN="client/build/HamDeck Remote.app/Contents/MacOS/HamDeck Remote"
# ⚠️ NO FALLBACK TO A BARE client/build/hamdeck-qml. There used to be one, and
# it would now hide the thing most likely to break: if OUTPUT_NAME stops
# applying, the bundle is called hamdeck-qml.app again and a fallback would
# quietly build, test and ship the badly-named app that this rename exists to
# prevent. On APPLE the output is always a bundle, so a miss is a real fault.
[ -f "$BIN" ] || { echo "no binary at $BIN - the bundle is not named as expected"; ls client/build; exit 1; }
echo "architectures: $(lipo -archs "$BIN")"
lipo -archs "$BIN" | grep -q arm64 || { echo "missing arm64"; exit 1; }
lipo -archs "$BIN" | grep -q x86_64 || { echo "missing x86_64"; exit 1; }

# ⚠️ A BUILD IS NOT A BUNDLE. Everything above proves the binary is universal and
# runs; none of it looks at what macOS actually shows the operator. 0.1.29 passed
# every one of those checks and shipped an app Finder called "hamdeck-qml" with the
# blank generic icon, plus no microphone usage string - which SIGKILLs the app on
# the first PTT. This is the check that was missing.
- name: Is it a properly formed Mac application
run: python3 tools/check_macos_bundle.py "client/build/HamDeck Remote.app"

# ⚠️ A TEMPORARY KEYCHAIN, not the login keychain. The runner is shared
# infrastructure; importing a signing identity into the default keychain leaves it
# for whatever runs next. This one is created, used, and destroyed with the job.
Expand Down Expand Up @@ -631,7 +653,7 @@ jobs:
if: ${{ env.APPLE_CERT_P12 != '' }}
run: |
set -euo pipefail
APP=client/build/hamdeck-qml.app
APP="client/build/HamDeck Remote.app"
[ -d "$APP" ] || { echo "no .app was built at $APP"; exit 1; }
macdeployqt "$APP" -qmldir=client/qml
# ⚠️ --deep signs the frameworks macdeployqt just copied in. Signing only the
Expand All @@ -644,6 +666,13 @@ jobs:
codesign --verify --deep --strict --verbose=2 "$APP"
echo "APP=$APP" >> "$GITHUB_ENV"

# ⚠️ AND AGAIN ON THE SIGNED BUNDLE. macdeployqt rewrites the bundle after the
# first check ran - it copies frameworks in and edits Info.plist - so the thing
# verified above is not the thing that goes into the DMG.
- name: Still a properly formed Mac application after macdeployqt
if: ${{ env.APPLE_CERT_P12 != '' }}
run: python3 tools/check_macos_bundle.py "$APP"

# ⚠️ A DMG WITH AN APPLICATIONS ALIAS - the idiomatic macOS install, and safer than a
# zip in practice: an unzipped .app tends to get run from Downloads, where quarantine
# behaves differently and the app is one cleanup away from vanishing. A DMG steers
Expand Down
32 changes: 31 additions & 1 deletion CARRYOVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,40 @@ Auth: session cookie, also accepted as `?token=` or `Bearer`. Since v3.4.14 stat
audio require a session. `web_admin_only=true` makes `/` serve the admin page and 404s the old
browser rig UI.

🔴 **TRAILING SLASHES ARE TRIMMED ON `/api/` PATHS — `ApiServer.cs:764-766`:**

```csharp
if (path.StartsWith("/api/")) { var trimmed = path.TrimEnd('/'); ... }
```

**The Stream Deck sends them.** Its amp tune button requests `/api/tune/amp/`, and the C++ host
matched that against the PREFIX route rather than the exact one — the not-configured catch-all,
which answers 200 and never tunes. The button reported success and did nothing, independent of
any permission.

⚠️ **A route inventory cannot see this.** `AUDIT-CSHARP.md` ticked `/api/tune/amp` because the
route exists, and the 71/74 sweep drove clean paths. How a path is MATCHED is part of the
contract, not an implementation detail. Normalise where the request is built, so the gates and
the router agree — at the router alone, `/api/ptt/on/` skips `IsTransmitRoute` and still
dispatches.

⚠️ **`/api/tune` is the rig's INTERNAL ATU and is the wrong tuner for this station.** The right
one is `/api/tune/tgxl`. Keep them separate and name them in any confirmation.
⚠️ **`/api/tune/amp` refuses every remote caller** (`AmpTuneOrDeny(isLocal)`). Do not expose it
remotely; a button that always errors is worse than a missing one.

⚠️ **AND IT ANSWERS 200 WHEN IT REFUSES.** `AmpTuneOrDeny` returns an error *object*, not an
error status, so a Stream Deck button reads the refusal as success: green tick, no tune. That
is not a detail - it is why this went unnoticed for weeks after the host moved boxes.

📌 **The C++ host deliberately diverges here, 09/01/2026.** `isLocal` was the right test on the
reference host *because it ran on the station PC*, so loopback proved an operator was present
and all 44 Stream Deck buttons hitting `localhost:5001` were local. Once the rig moved to its
own box, loopback started proving the caller was on the **rig** box - the one place nobody
sits - and amp tune became unreachable from the operating position. The C++ host therefore
asks **who**, not **where**: the loopback console, or a session whose account carries
`is_station`. It refuses with **403**. See `tools/amp_gate_check.sh`.

---

## 3. Audio — the whole reason a C++ port is interesting
Expand Down Expand Up @@ -181,7 +210,8 @@ Both lived only in the WPF host and were never ported to Linux. Check for more o
needs a second receiver or a net report.
- **WPF cannot be cross-compiled.** `Microsoft.NET.Sdk.WindowsDesktop` does not exist for Linux.
(Irrelevant to a C++ client, but it is why the .NET client is built on a Windows CI runner.)
- **Amp tune is local-only** — see §2.
- **Amp tune is local-only** — see §2. (On the C++ host: local console *or* an `is_station`
account, and the refusal is a 403. The restriction did not go away; the question changed.)

---

Expand Down
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ add_test(NAME cw COMMAND test_cw)
add_executable(test_transmit_gate tests/test_transmit_gate.cpp src/transmit_routes.cpp)
add_test(NAME transmit_gate COMMAND test_transmit_gate)

# ⚠️ Drives the REAL binary over HTTP, because the amp gate's failure mode was a
# refusal served as HTTP 200 - which every unit test and every route inventory
# read as success while the Stream Deck button sat dead. Needs the host built.
add_test(NAME amp_gate COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/amp_gate_check.sh
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
set_tests_properties(amp_gate PROPERTIES DEPENDS transmit_gate)

add_executable(test_remote_active tests/test_remote_active.cpp src/auth.cpp)
target_link_libraries(test_remote_active PRIVATE OpenSSL::Crypto)
add_test(NAME remote_active COMMAND test_remote_active)
Expand Down
Loading
Loading