From 3dcb22af628deb09d23b7ea287194df89824cd2c Mon Sep 17 00:00:00 2001 From: wa0o Date: Wed, 2 Sep 2026 15:55:59 +0000 Subject: [PATCH 1/2] Make the repository readable by someone who did not build it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Everything a visitor lands on was missing. There was no README, no LICENSE and no SECURITY.md, so the first file in the listing was AUDIT-CSHARP.md - notes written to whoever was next at this keyboard, addressed to the operator by name. - README.md, with a real screenshot of the client connected to a rig rather than a placeholder: what it is, the download table, how host and client divide responsibility, and an explicit "what is actually tested" section. A remote transmitter is a bad place for optimism, so the untested parts are named. - LICENSE: MIT, already settled in CLAUDE.md, plus the third-party notices that matter - Qt is LGPL-3.0 and linked DYNAMICALLY, which is what lets an MIT project distribute it at all. - SECURITY.md: what is authenticated, what must never be exposed, and the safety properties that should not be removed. Not boilerplate - this software keys a transmitter, and an earlier .NET version bound its Wavelog bridge to the whole LAN unauthenticated where a bare GET retuned the radio. - The engineering notes move to docs/internal/ with a README saying what they are. They stay public deliberately: the reason a fix worked, and the measurement that proved it, is worth more than a tidy summary. Every reference to them across ten files was repointed rather than left dangling. ⚠️ WIP.md opened with "THIS REPO IS PUBLIC" while the repo was private. The rule was right and the fact was wrong, which is how a rule stops being taken seriously right before it starts mattering. Corrected to "TREAT THIS REPO AS PUBLIC", with a note about why the wording changed. Also scrubbed the two remaining station hostnames, both in a Windows carryover note describing a tunnel that file already says is dead. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EqubrbjLXwz1GrVgmTcrf9 --- .github/workflows/release.yml | 2 +- CLAUDE.md | 10 +- CMakeLists.txt | 2 +- LICENSE | 33 ++++++ README.md | 98 ++++++++++++++++++ SECURITY.md | 58 +++++++++++ client/qml/HamDeck/ConnectPanel.qml | 2 +- client/src/ptt_hotkey.h | 2 +- client/src/rx_audio.h | 4 +- client/src/settings.h | 2 +- docs/AUDIT-AUDIO-ATTRIBUTION.md | 2 +- docs/images/panel.png | Bin 0 -> 63452 bytes .../internal/AUDIT-CSHARP.md | 0 .../internal/AUDIT-WAVELOG.md | 0 CARRYOVER.md => docs/internal/CARRYOVER.md | 0 .../internal/DAY-08-30-2026.md | 0 docs/internal/README.md | 21 ++++ WIP.md => docs/internal/WIP.md | 9 +- packaging/hamdeck.iss | 2 +- pusher/CARRYOVER-WINDOWS.md | 4 +- src/alsa_audio.cpp | 2 +- src/alsa_audio.h | 4 +- src/api.cpp | 20 ++-- src/audio.h | 2 +- src/cat.h | 2 +- src/cat_sim.cpp | 2 +- src/cat_sim.h | 2 +- src/config.h | 2 +- src/http.h | 2 +- src/main.cpp | 8 +- src/radio.h | 6 +- src/recorder.h | 2 +- src/serial_cat.h | 4 +- src/tgxl.h | 2 +- src/tx_audio.cpp | 2 +- src/tx_audio.h | 4 +- sync.sh | 2 +- tests/test_recorder.cpp | 2 +- tests/test_staleness.cpp | 2 +- tests/test_watchdog.cpp | 2 +- .../__pycache__/parity_check.cpython-312.pyc | Bin 10051 -> 10065 bytes tools/backup.sh | 2 +- tools/parity_check.py | 2 +- tools/walk_all_routes.py | 2 +- 44 files changed, 272 insertions(+), 59 deletions(-) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 docs/images/panel.png rename AUDIT-CSHARP.md => docs/internal/AUDIT-CSHARP.md (100%) rename AUDIT-WAVELOG.md => docs/internal/AUDIT-WAVELOG.md (100%) rename CARRYOVER.md => docs/internal/CARRYOVER.md (100%) rename DAY-08-30-2026.md => docs/internal/DAY-08-30-2026.md (100%) create mode 100644 docs/internal/README.md rename WIP.md => docs/internal/WIP.md (99%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d573ac..a6bd635 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ name: release # ⚠️ EVERY JOB RUNS THE BINARY BEFORE PACKAGING IT. A green build proves the code # compiles and proves nothing about whether the program starts - the .NET client # shipped a release that could not launch at all while every test passed -# (CARRYOVER.md section 8). `--selftest` walks the startup path and exits, and it +# (docs/internal/CARRYOVER.md section 8). `--selftest` walks the startup path and exits, and it # runs under a timeout because a hang is a failure too. on: diff --git a/CLAUDE.md b/CLAUDE.md index d38dfdd..f59c7ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ Windows client stops working. ## Read these first -- **`CARRYOVER.md`** (this repo) — the API surface, the audio chain with measured numbers, +- **`docs/internal/CARRYOVER.md`** (this repo) — the API surface, the audio chain with measured numbers, the three PTT traps, and a list of things that are **not possible** so they don't get retried. Written from measurements on the live station, not recollection. - **`~/hamdeck-site/brand/BRAND.md`** — the visual identity. **Anything with a user @@ -38,7 +38,7 @@ Judge proposals on whether they make the C++ build better, not on whether C++ wa a single density scale (`Theme.u()`/`f()`, from `Backend::uiScale`) *and* reflow against the width actually available (`Theme.cols()`). No unscaled pixel constants. Prove it with `--check-resolutions`, which measures every key at seven screen sizes — and look at the - PNGs it writes. See WIP.md §8d, including the four ways that walk passed while measuring + PNGs it writes. See docs/internal/WIP.md §8d, including the four ways that walk passed while measuring nothing. - Suggested libraries, all OSI-approved so code signing stays possible: miniaudio (audio), cpp-httplib (REST), IXWebSocket, nlohmann/json, Dear ImGui if a lighter UI is wanted. @@ -58,7 +58,7 @@ now rather than later: 96 kB/s; the crypto is the load. Size the board for that. A Pi Zero 2 W or better is the realistic floor. - The USB codec (TI PCM2903C) and the CP2105 CAT bridge both need `linux-modules-extra` on a - stripped kernel — see CARRYOVER.md. Do not assume the Pi image ships `snd-usb-audio`. + stripped kernel — see docs/internal/CARRYOVER.md. Do not assume the Pi image ships `snd-usb-audio`. ## Non-negotiables @@ -76,7 +76,7 @@ now rather than later: Six bugs stood between "it compiles" and "a voice on the air". **Not one was a language or a build problem, and every single one looked healthy to the checks that existed.** Full account in -`WIP.md` §8f–§8h; these are the rules that came out of it. +`docs/internal/WIP.md` §8f–§8h; these are the rules that came out of it. ### Counting is not checking Frames accepted, `hw_ptr` advancing at 48 kHz, zero drops, a queue behaving — **all of it reads @@ -93,7 +93,7 @@ the project, because a zero-height item lays out and paints perfectly well. `tes drags one with synthetic mouse events. Do that for any control that matters. ### Comparing route INVENTORIES is not comparing behaviour -`AUDIT-CSHARP.md` ticked `/api/remote-tx/on` because the route existed. It answered `200` and +`docs/internal/AUDIT-CSHARP.md` ticked `/api/remote-tx/on` because the route existed. It answered `200` and changed nothing, and the status route beside it **invented all three of its fields** so the two agreed with each other. ⚠️ **The test that catches this: call the route, then read the radio back through something that is NOT the route under test.** diff --git a/CMakeLists.txt b/CMakeLists.txt index 5213c7c..8d08b6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,7 +43,7 @@ add_executable(hamdeck-host src/main.cpp src/log.cpp src/amp_tuner.cpp src/api.c target_link_libraries(hamdeck-host PRIVATE civetweb-c-library OpenSSL::Crypto nlohmann_json::nlohmann_json asound) target_compile_options(hamdeck-host PRIVATE -Wall -Wextra) -# CI must RUN things, not just build them (CARRYOVER.md section 8): the .NET +# CI must RUN things, not just build them (docs/internal/CARRYOVER.md section 8): the .NET # client shipped a release that could not launch while every test passed. enable_testing() add_executable(test_staleness tests/test_staleness.cpp src/cat_sim.cpp src/radio.cpp src/session_stats.cpp) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..260cc7f --- /dev/null +++ b/LICENSE @@ -0,0 +1,33 @@ +MIT License + +Copyright (c) 2026 Henry Wussler (WA0O) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Third-party components distributed with the binaries: + + Qt 6 LGPL-3.0. Linked DYNAMICALLY and its licence text ships with + every package, which is what keeps this MIT project able to + distribute it. Do not link Qt statically without a commercial + licence. + SIL OFL fonts SIL Open Font License 1.1. The licence travels with any binary + that embeds them; see client/fonts/OFL.txt. + civetweb MIT. nlohmann/json MIT. OpenSSL Apache-2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d02174 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# HamDeck + +Operate an HF station from another room, or another country. A small always-on +host sits at the radio doing CAT control and audio; a desktop client shows the +panel and keys the transmitter. + +![The HamDeck client, connected](docs/images/panel.png) + +Built for and tested against a **Yaesu FTDX-101MP**. Other Yaesu CAT radios are +likely close, but nothing else has been on the air with it — see +[What is actually tested](#what-is-actually-tested). + +## Download + +Installers for every platform are on the +[latest release](https://github.com/jwussler/hamdeck-releases/releases/latest). + +| | | +|---|---| +| **Windows** | `HamDeck-win-Setup.exe` — client **and** the Wavelog pusher, one installer, updated together | +| **Windows, client only** | `HamDeckRemote-win-Setup.exe` — just the radio panel, no auto-update | +| **macOS** | `HamDeckRemote-macOS.dmg` — signed, notarised and stapled; drag to Applications | +| **Linux** | `hamdeck-client__amd64.deb` / `_arm64.deb` | + +Everything is code-signed. On Windows the publisher reads **Henry Wussler**; +SmartScreen may still warn, because it asks *"have I seen this file before?"* +rather than *"is this safe?"*, and a new release from a small publisher always +starts at no. + +## How it fits together + +``` + radio ──USB──┤ host ├── CAT + audio ──── HTTP / WebSocket ────┤ client │ + (CAT + (any always-on Linux box; (Windows, macOS, + USB audio) a Raspberry Pi is the target) Linux) +``` + +**The host is the authority. The client is a display that asks.** Every limit +that matters lives at the radio, because a client can be closed, crashed, or run +from a laptop that goes to sleep mid-transmission: + +- a **transmit watchdog** drops PTT after a timeout and confirms with the radio + that it actually stopped +- **power returns to the local cap** when a remote client disconnects, so nobody + walks up to a radio and drives an amplifier with twice the power they expect +- RX is **muted while you are keyed** — hearing your own voice back at ~220 ms is + delayed auditory feedback, and it makes people slur and stutter + +## What it does + +- Full panel: twin VFO, band and mode, S-meter, filters, RIT/XIT, AGC, + attenuator, preamp, noise blanker and reduction, antenna selection +- **Receive audio** streamed to the client, and **transmit audio** back to the rig +- **PTT** by hotkey or on-screen, with the watchdog above behind it +- **Recording** — continuous, plus a pre-trigger replay buffer that saves what + happened *before* you pressed anything. Every recording writes a JSON sidecar + with UTC times, frequency and mode, so it can be matched to a log later +- **Wavelog** integration on Windows: the log follows the radio +- A local REST API on loopback, so **Stream Deck** buttons work + +## What is actually tested + +Being straight about this, because a remote transmitter is not a good place for +optimism: + +| | | +|---|---| +| Radio | Yaesu FTDX-101MP, over its USB CAT + audio codec | +| Host | Ubuntu 24.04, x86-64. ARM64 builds; a Pi is the intended home but has not run a station yet | +| Client | Windows x64, macOS (universal), Linux x64/ARM64 | +| Not implemented | CW keyer, voice memories, RX antenna switching — those buttons say so rather than failing silently | +| Not built | Windows on ARM native (x64 runs under emulation) | + +## Building + +```sh +# host (Linux) +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build + +# client (Windows, macOS, Linux) — needs Qt 6.8+ +cmake -S client -B client/build -DCMAKE_BUILD_TYPE=Release && cmake --build client/build +ctest --test-dir client/build +``` + +## Before you expose it + +Read [SECURITY.md](SECURITY.md). The short version: the API port is meant for +**loopback**, the dashboard needs a session, and anything reachable from outside +your LAN belongs behind a tunnel or a reverse proxy that terminates TLS. + +Remote operation does not change whose callsign is on the air. Control of the +transmitter, and the obligation to identify and stay in band, stay with the +operator. + +## Licence + +MIT — see [LICENSE](LICENSE). Qt is LGPL-3.0 and is linked dynamically, with its +licence text shipped alongside every binary. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e833e70 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,58 @@ +# Security + +HamDeck keys a transmitter over a network. That is not a normal web app threat model, +so this page says plainly what is protected, what is not, and what must never be +exposed. Read it before putting any part of this on a public address. + +## Reporting a problem + +Open a GitHub issue for anything already public. For something that would let a +stranger transmit on someone else's licence, **do not open an issue** — use GitHub's +private vulnerability reporting on this repository so there is time to ship a fix. + +## The trust model, in one line + +**The host is the authority. The client is a display that asks.** Every limit that +matters — the transmit watchdog, the local power cap, who may transmit at all — lives +in the host, because a client can be closed, crashed, or driven from a laptop that +went to sleep mid-transmission. + +## What is authenticated + +| surface | who can reach it | +|---|---| +| dashboard port, `/ws`, `/ws/tx` | session required (login, or a `hamdeck_session` cookie) | +| `/api/health` | **no session** — liveness and tuner state only, deliberately | +| the API port | intended for **loopback**: local tools such as Stream Deck | +| `/api/admin/*` | an admin account | +| amp tune | local console, or an account explicitly marked as the station | + +⚠️ **`allow_anonymous_status` removes the session requirement from receive audio.** +It exists for a reason and it is off by default. Turning it on to make a page work is +the wrong fix — log in instead. + +## What must not be exposed + +- **Do not put the API port on a LAN address or the internet.** It is designed for + loopback. An earlier .NET version of this project bound its Wavelog bridge to + `http://+:54321/` with no authentication at all, where a bare `GET /14074000` + retuned the radio. That is the mistake this section exists to prevent. +- **Put any remote access behind a tunnel or a reverse proxy that terminates TLS.** + The host speaks plain HTTP and WebSockets and does not pretend otherwise. +- **Do not expose the CAT proxy.** It is a serial port to the radio wearing a socket. + +## Safety properties you should not remove + +- **The transmit watchdog** drops PTT after `ptt_timeout_seconds` (default 180) and + confirms with the radio that it actually stopped. Without it, a dropped link leaves + the rig keyed with nobody watching. +- **Power returns to the local cap when a remote client disconnects**, so nobody walks + up to a radio and drives an amplifier with twice the power they expect. +- **Recording is off unless a path is configured**, and PTT auto-record is off unless + it is switched on. It records whoever you are talking to. + +## Licensed-operator responsibility + +Remote operation does not change whose callsign is on the air. Control of the +transmitter, and the obligation to identify and to stay in band and licence class, stay +with the operator. Nothing here supervises that for you. diff --git a/client/qml/HamDeck/ConnectPanel.qml b/client/qml/HamDeck/ConnectPanel.qml index 11860cf..1f70b7a 100644 --- a/client/qml/HamDeck/ConnectPanel.qml +++ b/client/qml/HamDeck/ConnectPanel.qml @@ -6,7 +6,7 @@ import HamDeck // The connect screen. // // ⚠️ THERE IS NO DEFAULT HOST, AND THERE MUST NOT BE. A hostname compiled into a -// public repo points every install at that station (CARRYOVER.md section 6). The +// public repo points every install at that station (docs/internal/CARRYOVER.md section 6). The // field starts empty on a fresh install and is remembered afterwards. // // ⚠️ THE PASSWORD IS NEVER REMEMBERED. Host and username are; the password costs diff --git a/client/src/ptt_hotkey.h b/client/src/ptt_hotkey.h index 0c09fff..1b2e8a2 100644 --- a/client/src/ptt_hotkey.h +++ b/client/src/ptt_hotkey.h @@ -23,7 +23,7 @@ // // ⚠️ AUTO-REPEAT MUST BE SUPPRESSED. A held key repeats at the OS repeat rate, // and without filtering that flaps the transmitter on and off many times a -// second (CARRYOVER.md section 6 records this as mandatory on Windows via +// second (docs/internal/CARRYOVER.md section 6 records this as mandatory on Windows via // MOD_NOREPEAT). Handled here for every platform, not just Windows. #include diff --git a/client/src/rx_audio.h b/client/src/rx_audio.h index 461207e..aaa4492 100644 --- a/client/src/rx_audio.h +++ b/client/src/rx_audio.h @@ -21,7 +21,7 @@ class RxAudio : public QObject { ~RxAudio() override; // `device_name` is a device DESCRIPTION, not an index. Empty means the system - // default. CARRYOVER.md section 6: indices shift when USB devices come and go, + // default. docs/internal/CARRYOVER.md section 6: indices shift when USB devices come and go, // which is what produced a dead microphone; and index 0 is not "the default", // it is arbitrary and out of range when there are no devices at all. void Start(const QString& ws_url, const QString& device_name); @@ -32,7 +32,7 @@ class RxAudio : public QObject { // ⚠️ Muted while the rig is keyed. Hearing yourself at the round-trip delay is // delayed auditory feedback - it disrupts speech so reliably that speech labs // use it deliberately. The operator slurs, hears themselves doing it, and - // reports the link as broken (CARRYOVER.md section 4c). + // reports the link as broken (docs/internal/CARRYOVER.md section 4c). // // Driven off the rig's own tx state so every PTT source behaves alike, and // DROPS what queued on unmute so they come back live rather than replaying. diff --git a/client/src/settings.h b/client/src/settings.h index 98b20db..47c7acc 100644 --- a/client/src/settings.h +++ b/client/src/settings.h @@ -2,7 +2,7 @@ // Client settings. // -// ⚠️ THREE RULES FROM CARRYOVER.md SECTION 6, ALL LEARNED THE HARD WAY: +// ⚠️ THREE RULES FROM docs/internal/CARRYOVER.md SECTION 6, ALL LEARNED THE HARD WAY: // // 1. Settings live OUTSIDE the install directory, so an update cannot overwrite // them. QSettings with the platform config location does this. diff --git a/docs/AUDIT-AUDIO-ATTRIBUTION.md b/docs/AUDIT-AUDIO-ATTRIBUTION.md index 739af81..73284d3 100644 --- a/docs/AUDIT-AUDIO-ATTRIBUTION.md +++ b/docs/AUDIT-AUDIO-ATTRIBUTION.md @@ -78,4 +78,4 @@ the NetLogger sync) show medians of 206s and 63s and only ~13% under 20s, while the single biggest term in the slicing error and it has not been measured. Related: `qsl-card-system` (the QR spot on the card is the consumer of this), -[[netlogger-xml-api]], and section 1 of CARRYOVER.md for the recorder itself. +[[netlogger-xml-api]], and section 1 of docs/internal/CARRYOVER.md for the recorder itself. diff --git a/docs/images/panel.png b/docs/images/panel.png new file mode 100644 index 0000000000000000000000000000000000000000..9d3129474c83bffec3a89ec85ea4503698d8a82e GIT binary patch literal 63452 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 literal 0 HcmV?d00001 diff --git a/AUDIT-CSHARP.md b/docs/internal/AUDIT-CSHARP.md similarity index 100% rename from AUDIT-CSHARP.md rename to docs/internal/AUDIT-CSHARP.md diff --git a/AUDIT-WAVELOG.md b/docs/internal/AUDIT-WAVELOG.md similarity index 100% rename from AUDIT-WAVELOG.md rename to docs/internal/AUDIT-WAVELOG.md diff --git a/CARRYOVER.md b/docs/internal/CARRYOVER.md similarity index 100% rename from CARRYOVER.md rename to docs/internal/CARRYOVER.md diff --git a/DAY-08-30-2026.md b/docs/internal/DAY-08-30-2026.md similarity index 100% rename from DAY-08-30-2026.md rename to docs/internal/DAY-08-30-2026.md diff --git a/docs/internal/README.md b/docs/internal/README.md new file mode 100644 index 0000000..62cdeae --- /dev/null +++ b/docs/internal/README.md @@ -0,0 +1,21 @@ +# Internal engineering notes + +Working notes, not documentation. They are written to whoever is next at this +keyboard — candid, dated, full of things that turned out to be wrong, and +addressed to the operator by name in places. + +They are kept public deliberately. The reason a fix worked, and the measurement +that proved it, is more useful than a tidy summary that leaves out how long it +took to find. Several of these files exist because a green build lied. + +| file | what it is | +|---|---| +| `CARRYOVER.md` | the API surface, the audio chain with measured numbers, and a list of things that are **not possible** so nobody retries them | +| `WIP.md` | running build log — every trap, in the order it bit | +| `AUDIT-CSHARP.md` | walking the C# implementation down before writing anything | +| `AUDIT-WAVELOG.md` | the same for the Wavelog bridge | +| `DAY-08-30-2026.md` | one day's account, kept because the failure modes repeat | + +⚠️ Nothing station-specific belongs in this repository — no hostnames, +addresses, VM ids or tunnel details. Site detail lives in a gitignored +`SITE.md`. That applies to commit messages too. diff --git a/WIP.md b/docs/internal/WIP.md similarity index 99% rename from WIP.md rename to docs/internal/WIP.md index bceb5bd..d6ecb24 100644 --- a/WIP.md +++ b/docs/internal/WIP.md @@ -2,9 +2,12 @@ Mid-build handover. Written 08/30/2026. Read §1 and §2 before touching anything. -> **THIS REPO IS PUBLIC.** Nothing station-specific goes in it — no hostnames, addresses, VM -> ids, tunnel details, or live readings that say what the station was doing. That applies to -> **commit messages** too. Site detail lives in the gitignored `SITE.md`. +> **TREAT THIS REPO AS PUBLIC.** Nothing station-specific goes in it — no hostnames, +> addresses, VM ids, tunnel details, or live readings that say what the station was doing. +> That applies to **commit messages** too. Site detail lives in the gitignored `SITE.md`. +> ⚠️ This banner said "THIS REPO IS PUBLIC" for months while the repo was private. The rule +> was right and the fact was wrong, which is how a rule stops being taken seriously right +> before it starts mattering. It is public now; the wording is the instruction either way. > CARRYOVER.md §6 states the narrow version: *a hostname in a public repo points every install > at that station.* It generalises. diff --git a/packaging/hamdeck.iss b/packaging/hamdeck.iss index c1b6778..fe2a09a 100644 --- a/packaging/hamdeck.iss +++ b/packaging/hamdeck.iss @@ -3,7 +3,7 @@ ; ⚠️ THIS IS BUILT ON WINDOWS, NOT CROSS-COMPILED. Qt does not cross-compile ; comfortably, and more importantly a Windows binary that has never run on ; Windows is exactly the release the .NET client shipped that could not launch at -; all while every test passed (CARRYOVER.md section 8). The CI job runs the +; all while every test passed (docs/internal/CARRYOVER.md section 8). The CI job runs the ; binary on the runner before packaging it. ; ; ⚠️ UNSIGNED. Windows SmartScreen will warn on first run. Saying so is the diff --git a/pusher/CARRYOVER-WINDOWS.md b/pusher/CARRYOVER-WINDOWS.md index a34e5c3..07be4cd 100644 --- a/pusher/CARRYOVER-WINDOWS.md +++ b/pusher/CARRYOVER-WINDOWS.md @@ -134,7 +134,7 @@ including this pusher**. It converts a transient process conflict into a permane ### The actual fix Stop the legacy C# host. Checks done first, so this is safe rather than hopeful: `rig_connected:false` (the rig lives on **deck**, `192.168.40.64` now), and the local -cloudflared tunnel that used to publish its `:5002` as `radio.wa0o.com` **is not running on +cloudflared tunnel that used to publish its `:5002` as the station hostname **is not running on this PC** — so nothing public depends on it. Stop-Process -Name HamDeck -Force @@ -360,7 +360,7 @@ misleading. Do not re-run it as written; read §4 first.** ### Next, in order 1. 🔴 **Remove `HamDeck.lnk` from `shell:startup`** (or the C# host retakes 5001 at the next logon and the deck dies again with the same misleading error). Joe's call, because it also - ends the local `radio.wa0o.com` origin — which is already dead here anyway, since + ends the local the station hostname origin — which is already dead here anyway, since `cloudflared` is not running on this PC. 2. **Press real Stream Deck buttons and report which work.** Expect **CW keyer**, **voice memories** and **RX antenna** to fail — known unported features (§6), not new bugs. diff --git a/src/alsa_audio.cpp b/src/alsa_audio.cpp index eba4f5b..d367f25 100644 --- a/src/alsa_audio.cpp +++ b/src/alsa_audio.cpp @@ -117,7 +117,7 @@ bool AlsaPlayback::Open(const std::string& device, int sample_rate) { return false; } // ⚠️ A GENEROUS BUFFER, WITH THE FILL LEVEL CONTROLLED ELSEWHERE. - // CARRYOVER.md section 3: give the device room, then manage how full it is. + // docs/internal/CARRYOVER.md section 3: give the device room, then manage how full it is. // A small buffer just trades latency for underruns, and an underrun mid-word // is worse than 100 ms of delay. error_ = Configure(pcm_, sample_rate, sample_rate / 2, sample_rate / 50); diff --git a/src/alsa_audio.h b/src/alsa_audio.h index ca85a48..4fb3db7 100644 --- a/src/alsa_audio.h +++ b/src/alsa_audio.h @@ -12,7 +12,7 @@ // 8000 returns 11025 worth of data, silently. // playback device altsets advertise 32000, 44100, 48000 only. // default playback buffer measured 24048 frames at 48 kHz = 501 ms, matching -// the ~500 ms CARRYOVER.md section 3 records. +// the ~500 ms docs/internal/CARRYOVER.md section 3 records. #include @@ -60,7 +60,7 @@ class AlsaPlayback : public TxAudioSink { // that is always ~0, because the sender sends at real time - it reports // "nothing queued" while half a second sits in ALSA, and an estimate whose // failure mode is zero looks exactly like a working measurement - // (CARRYOVER.md section 3). That bug cut the end off every transmission. + // (docs/internal/CARRYOVER.md section 3). That bug cut the end off every transmission. long QueuedFrames() const; int QueuedMs() const override; long Xruns() const override { return xruns_; } diff --git a/src/api.cpp b/src/api.cpp index 5af3422..bab7bf9 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -237,7 +237,7 @@ std::string Pad(long long v, int width) { // ⚠️ POWER CAP, PORTED FAITHFULLY AND DELIBERATELY NOT "FIXED". // In the C# host a LOCAL caller is capped at 100 W while a remote caller gets // 200 W - which reads backwards, so it is exactly the kind of thing to port -// as-is and ask about rather than quietly invert. Flagged in WIP.md. +// as-is and ask about rather than quietly invert. Flagged in docs/internal/WIP.md. // Hard ceiling on how long unkeying may be delayed, whatever the buffer says. constexpr int kMaxDrainMs = 1200; @@ -357,7 +357,7 @@ void InstallRoutes(HttpServer& server, Listener listener, int bound_port, // checkable answer is. The reference host does not serve this route, so it // 404s there and any such tool must refuse on 404 rather than assume. // ── Capability reporting ─────────────────────────────────────────────────── - // ⚠️ CARRYOVER.md section 1: on the reference Linux build /api/record/start + // ⚠️ docs/internal/CARRYOVER.md section 1: on the reference Linux build /api/record/start // answers {"status":"ok","recording":true} while Start() sets IsRecording = // false. A 200 there means the route exists, not that anything is recording. // @@ -367,11 +367,11 @@ void InstallRoutes(HttpServer& server, Listener listener, int bound_port, // reads the same shape. The VALUES are honest: nothing is recording and there // is no capture backend, which is what available:false says. // - // ⚠️ CARRYOVER.md section 1: the reference /api/record/start answers + // ⚠️ docs/internal/CARRYOVER.md section 1: the reference /api/record/start answers // {"status":"ok","recording":true} while Start() sets IsRecording = false. The // only honest signal is this route's file_recording. Do not reproduce the lie. // ⚠️ Every field here is derived from what ACTUALLY happened, never from - // having been asked. CARRYOVER.md section 1: the reference /api/record/start + // having been asked. docs/internal/CARRYOVER.md section 1: the reference /api/record/start // answers ok/recording:true while Start() sets IsRecording = false. { Recorder* rec = deps.recorder; @@ -467,7 +467,7 @@ void InstallRoutes(HttpServer& server, Listener listener, int bound_port, } // Voice keyer: present on the reference host and answering, contrary to the - // note in CARRYOVER.md section 1 that lists it among the null services. + // note in docs/internal/CARRYOVER.md section 1 that lists it among the null services. server.Get("/api/voice/status", [](const HttpRequest&, HttpResponse& res) { WriteJson(res, 200, R"({"status":"ok","playing":false})"); }); @@ -737,7 +737,7 @@ void InstallRoutes(HttpServer& server, Listener listener, int bound_port, JsonBool(ok && deps.auth->IsAdmin(token)), JsonBool(ok && deps.auth->CanTransmit(token)), // ⚠️ So a client can GREY THE AMP TUNE BUTTON instead of showing a - // live one that answers 403. CARRYOVER.md section 2: "a button that + // live one that answers 403. docs/internal/CARRYOVER.md section 2: "a button that // always errors is worse than a missing one" - and it is also the // only way to confirm the right is live without keying an amplifier // to find out. @@ -768,7 +768,7 @@ void InstallRoutes(HttpServer& server, Listener listener, int bound_port, return; } - // The token goes in the cookie and NOT in the body - CARRYOVER.md section 2. + // The token goes in the cookie and NOT in the body - docs/internal/CARRYOVER.md section 2. // HttpOnly keeps it away from page scripts; SameSite=Strict closes the // top-level-navigation CSRF vector on state-changing GETs, of which this API // has many (/api/ptt/on is a GET). @@ -831,7 +831,7 @@ void InstallRoutes(HttpServer& server, Listener listener, int bound_port, // ⚠️ PTT ON is here; PTT OFF is below, because unkeying is not instant. Unkeying must wait for the audio still // queued in the ALSA buffer or the tail of every transmission is lost - // (CARRYOVER.md section 4a), and that wait needs the real device depth from + // (docs/internal/CARRYOVER.md section 4a), and that wait needs the real device depth from // /proc/asound. Shipping an unkey that drops PTT immediately would look // like it works and quietly cut the end off every over - the exact bug that // took a report from a net to find. It lands with the audio work. @@ -1086,7 +1086,7 @@ void InstallRoutes(HttpServer& server, Listener listener, int bound_port, // The last fraction of a second of every transmission is sitting in the ALSA // buffer when the operator releases PTT. Drop the carrier first and that audio // is never transmitted - the end of every over is cut off, and it sounds like - // the other station stopped listening. CARRYOVER.md section 4a; the bug it + // the other station stopped listening. docs/internal/CARRYOVER.md section 4a; the bug it // describes took a report from a net to find. // // ⚠️ WAIT THE DEPTH AT THIS MOMENT, NOT "UNTIL EMPTY". The microphone stays @@ -1444,7 +1444,7 @@ void InstallRoutes(HttpServer& server, Listener listener, int bound_port, hz, mode); }}); // ── Tuners: THREE different things, and confusing them is expensive ──────── - // ⚠️ /api/tune is the RIG'S INTERNAL ATU (AC002;). CARRYOVER.md section 2 is + // ⚠️ /api/tune is the RIG'S INTERNAL ATU (AC002;). docs/internal/CARRYOVER.md section 2 is // explicit that it is the WRONG tuner for this station; the right one is // /api/tune/tgxl. They are kept separate and each names itself in its reply so // a confirmation dialog cannot say "tuning" and leave the operator guessing diff --git a/src/audio.h b/src/audio.h index 6435ebc..a95a6ba 100644 --- a/src/audio.h +++ b/src/audio.h @@ -9,7 +9,7 @@ // ⚠️ The queue MUST be bounded and MUST drop, not block or grow. An unbounded // queue does not avoid loss, it converts loss into ever-growing latency, and // latency on a receive stream is indistinguishable from a broken link to the -// operator. CARRYOVER.md section 6 says it for the client side; it is just as +// operator. docs/internal/CARRYOVER.md section 6 says it for the client side; it is just as // true here. // // Which end gets dropped matters. Dropping the OLDEST keeps the stream current: diff --git a/src/cat.h b/src/cat.h index 21aff17..9da20a0 100644 --- a/src/cat.h +++ b/src/cat.h @@ -8,7 +8,7 @@ // station stays on the air. Assume it will outlive the port and be the thing CI // runs against, because CI will never have a radio. // -// ⚠️ The serial lock is NOT re-entrant across threads (CARRYOVER.md section 5). +// ⚠️ The serial lock is NOT re-entrant across threads (docs/internal/CARRYOVER.md section 5). // Nothing on a request thread may call into this. Only the poller does. #include diff --git a/src/cat_sim.cpp b/src/cat_sim.cpp index 1995321..e18ca22 100644 --- a/src/cat_sim.cpp +++ b/src/cat_sim.cpp @@ -17,7 +17,7 @@ std::string Pad(long long v, int width) { std::optional SimulatedRig::Exchange(const std::string& cmd) { std::lock_guard lock(mu_); - // ID; is the ONLY safe probe. CARRYOVER.md section 9: probing with a control + // ID; is the ONLY safe probe. docs/internal/CARRYOVER.md section 9: probing with a control // route once changed the operating mode mid-session. 0682 is the FTDX-101MP. if (cmd == "ID;") return "ID0682;"; // ⚠️ CW KEYER. Modelled because a route that CANNOT be exercised is a route nobody has diff --git a/src/cat_sim.h b/src/cat_sim.h index cbcaf82..9819f56 100644 --- a/src/cat_sim.h +++ b/src/cat_sim.h @@ -9,7 +9,7 @@ // // It deliberately does NOT simulate: audio (there is no audio over CAT), amp or // TGXL tuning (those are separate hardware), or timing. Anything about latency, -// buffer depth or PTT tail MUST be measured on the real station - CARRYOVER.md +// buffer depth or PTT tail MUST be measured on the real station - docs/internal/CARRYOVER.md // section 3 is explicit that estimates whose failure mode is zero look exactly // like working measurements. diff --git a/src/config.h b/src/config.h index 69e2d26..cb4aafc 100644 --- a/src/config.h +++ b/src/config.h @@ -7,7 +7,7 @@ // // ⚠️ NO DEFAULT SHIPS AN ADDRESS OR A HOSTNAME. The reference host's config // carries station LAN addresses as compiled-in defaults, and that source is -// public - CARRYOVER.md section 6 says a hostname in a public repo points every +// public - docs/internal/CARRYOVER.md section 6 says a hostname in a public repo points every // install at that station, and an address does the same. Here every such field // defaults to EMPTY, which means "not configured, feature off". An operator who // wants the feature says where it lives. diff --git a/src/http.h b/src/http.h index c2f2e52..361dd0f 100644 --- a/src/http.h +++ b/src/http.h @@ -48,7 +48,7 @@ using PreRouting = std::function; // ⚠️ Writes are serialised per connection. Two threads writing the same socket // interleave frames and corrupt the stream - the C# client hit the same wall from // the other side, where overlapping SendAsync calls are rejected outright -// (CARRYOVER.md section 6). Here the lock is taken inside Send*. +// (docs/internal/CARRYOVER.md section 6). Here the lock is taken inside Send*. class WsConnection { public: explicit WsConnection(mg_connection* conn) : conn_(conn) {} diff --git a/src/main.cpp b/src/main.cpp index 17f744f..a802448 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,7 @@ // HamDeck C++ host. // // Route surface is in api.cpp; this file owns process startup and the listener -// split. See WIP.md for the road map and CARRYOVER.md for the traps. +// split. See docs/internal/WIP.md for the road map and docs/internal/CARRYOVER.md for the traps. #include #include @@ -56,7 +56,7 @@ void OnStopSignal(int) { // The control port trusting every caller is exactly why it must be bound to the // loopback address: the trust boundary is then enforced by the kernel, not by a // check somebody can forget to write. /api/tune/amp must refuse every remote -// caller (CARRYOVER.md section 2), and "is this caller local" is answered by +// caller (docs/internal/CARRYOVER.md section 2), and "is this caller local" is answered by // WHICH SOCKET ACCEPTED IT - never by a header, which the caller controls. constexpr const char* kControlAddr = "127.0.0.1"; // ⚠️ DEFAULTS ONLY. The real values come from the config - api_port and @@ -71,7 +71,7 @@ constexpr int kDashPort = 5002; } // namespace // Walks the whole startup path - poller, audio, auth, both listeners - proves the -// process actually serves a request, and exits. CARRYOVER.md section 8: the .NET +// process actually serves a request, and exits. docs/internal/CARRYOVER.md section 8: the .NET // client shipped a release that could not launch at all while every test passed, // because CI built the artifact and never ran it. // @@ -186,7 +186,7 @@ int main(int argc, char** argv) { // Synthetic RX audio: the codec is passed through to the reference host, so // there is no real capture device here. 22050 Hz mono/16-bit matches the wire - // format the client expects (CARRYOVER.md section 2). + // format the client expects (docs/internal/CARRYOVER.md section 2). // RX source: the real codec when one is named, a tone otherwise. A failure to // open a NAMED device is fatal, for the same reason a missing CAT device is: // a host that silently substitutes a test tone for the receiver would have the diff --git a/src/radio.h b/src/radio.h index 6099528..cdf83d9 100644 --- a/src/radio.h +++ b/src/radio.h @@ -4,7 +4,7 @@ // // ⚠️ THE POINT OF THIS FILE. /api/status is served ENTIRELY from this cache and // never touches the serial port from a request thread - the serial lock is not -// re-entrant across threads (CARRYOVER.md section 5). The C# Linux host shipped +// re-entrant across threads (docs/internal/CARRYOVER.md section 5). The C# Linux host shipped // with no poller at all, so /api/status served a frequency 3.6 HOURS stale and a // tx:true left over from a tune while the rig was receiving. A cache with nothing // refreshing it is worse than no cache: it answers confidently and wrongly. @@ -79,7 +79,7 @@ class RadioPoller { long long CacheAgeMs() const; // ⚠️ THE ONLY WAY A REQUEST THREAD MAY TOUCH THE RADIO. - // The serial lock is not re-entrant across threads (CARRYOVER.md section 5), + // The serial lock is not re-entrant across threads (docs/internal/CARRYOVER.md section 5), // so exactly one thread - the poller - ever speaks to the port. Handlers queue // a command and return; the poller drains the queue at the top of each cycle. // Queueing also gives commands a natural ordering, which matters for pairs @@ -96,7 +96,7 @@ class RadioPoller { // Transmit watchdog. Zero disables it. // - // ⚠️ THIS MUST LIVE NEXT TO THE RADIO (CARRYOVER.md section 4b). A timeout in + // ⚠️ THIS MUST LIVE NEXT TO THE RADIO (docs/internal/CARRYOVER.md section 4b). A timeout in // the client or the browser protects nothing: close the tab, sleep the laptop // or lose the link while keyed and the rig stays keyed with nobody watching. // The Linux host shipped without this for months because it existed only in diff --git a/src/recorder.h b/src/recorder.h index 311dc5e..60d9024 100644 --- a/src/recorder.h +++ b/src/recorder.h @@ -2,7 +2,7 @@ // Recording, and the replay buffer. // -// ⚠️ CARRYOVER.md section 1 is about THIS FEATURE. On the reference Linux build +// ⚠️ docs/internal/CARRYOVER.md section 1 is about THIS FEATURE. On the reference Linux build // /api/record/start answers {"status":"ok","recording":true} while Start() sets // IsRecording = false. A 200 there means the route exists, not that anything is // recording, and the only honest signal is file_recording in the status route. diff --git a/src/serial_cat.h b/src/serial_cat.h index ea07836..27afe99 100644 --- a/src/serial_cat.h +++ b/src/serial_cat.h @@ -9,7 +9,7 @@ // would have caught for free. // // ⚠️ Only ONE thread may ever call this. The serial lock is not re-entrant -// across threads (CARRYOVER.md section 5); RadioPoller owns it and request +// across threads (docs/internal/CARRYOVER.md section 5); RadioPoller owns it and request // threads queue commands instead. #include @@ -27,7 +27,7 @@ class SerialCat : public CatTransport { // Tries each candidate and keeps the first that answers the identity probe. // - // ⚠️ The probe is `ID;` and ONLY `ID;`. CARRYOVER.md section 9: probing with a + // ⚠️ The probe is `ID;` and ONLY `ID;`. docs/internal/CARRYOVER.md section 9: probing with a // control route once changed the operating mode mid-session. This also matters // because the CP2105 is a DUAL UART - one physical device enumerates two // serial ports and only one of them is CAT - so the port has to be identified diff --git a/src/tgxl.h b/src/tgxl.h index 94550f1..9572f70 100644 --- a/src/tgxl.h +++ b/src/tgxl.h @@ -2,7 +2,7 @@ // TGXL — the external antenna tuner, reached over TCP. // -// ⚠️ THIS IS THE RIGHT TUNER FOR THIS STATION. CARRYOVER.md section 2 is +// ⚠️ THIS IS THE RIGHT TUNER FOR THIS STATION. docs/internal/CARRYOVER.md section 2 is // explicit: /api/tune is the rig's INTERNAL ATU and is the wrong one. They are // kept separate and each names itself in its reply, so a confirmation can never // say just "tuning" and leave the operator guessing which box is about to key up. diff --git a/src/tx_audio.cpp b/src/tx_audio.cpp index ae498ff..ecdbc54 100644 --- a/src/tx_audio.cpp +++ b/src/tx_audio.cpp @@ -65,7 +65,7 @@ bool TxAudioReceiver::Accept(const char* data, size_t bytes, bool keyed) { std::lock_guard lock(mu_); if (queue_.size() >= kMaxQueuedChunks) { - // ⚠️ TRIM ONLY BETWEEN OVERS (CARRYOVER.md section 3). Dropping audio while + // ⚠️ TRIM ONLY BETWEEN OVERS (docs/internal/CARRYOVER.md section 3). Dropping audio while // the rig is KEYED is audible - a syllable vanishes mid-sentence. Between // overs it costs nothing, and with the mic open there is always idle time. // So while keyed we accept the latency and let the queue run long; the diff --git a/src/tx_audio.h b/src/tx_audio.h index 79697b6..a965a1b 100644 --- a/src/tx_audio.h +++ b/src/tx_audio.h @@ -5,7 +5,7 @@ // ⚠️ THIS PATH PUTS A HUMAN VOICE ON THE AIR. Every rule here is about not // transmitting the wrong thing, or transmitting nothing while appearing to work. // -// Wire format is 48000 Hz / 16-bit / mono (CARRYOVER.md section 2). RX is 22050 +// Wire format is 48000 Hz / 16-bit / mono (docs/internal/CARRYOVER.md section 2). RX is 22050 // and TX is 48000 because the codec's capture supports 8000-48000 but its // PLAYBACK only supports 32000-48000 - the asymmetry is the device's, not a // choice. @@ -117,7 +117,7 @@ class TxAudioReceiver { // never grows, so the deadlock never happens. static constexpr size_t kMaxQueuedChunks = 50; - // ⚠️ ADAPTIVE BUFFERING (CARRYOVER.md section 3), and it is not optional. + // ⚠️ ADAPTIVE BUFFERING (docs/internal/CARRYOVER.md section 3), and it is not optional. // // Writing each chunk to the device the moment it arrives leaves no cushion: // the device runs dry between chunks and underruns. Measured on the real diff --git a/sync.sh b/sync.sh index 7134ad6..4c3c149 100755 --- a/sync.sh +++ b/sync.sh @@ -3,7 +3,7 @@ # The VM is the build host on purpose: it is where the ALSA and serial work will # run, so it is the only place a green build means anything. # -# No default host is baked in. CARRYOVER.md section 6: never ship a default host - +# No default host is baked in. docs/internal/CARRYOVER.md section 6: never ship a default host - # a hostname in a public repo points every install at that station. Set # HAMDECK_BUILD_HOST to an ssh target; the site's value is in the gitignored SITE.md. set -e diff --git a/tests/test_recorder.cpp b/tests/test_recorder.cpp index d97f4da..3e86377 100644 --- a/tests/test_recorder.cpp +++ b/tests/test_recorder.cpp @@ -1,6 +1,6 @@ // Recorder tests. // -// ⚠️ The point of these is CARRYOVER.md section 1: the reference host answers +// ⚠️ The point of these is docs/internal/CARRYOVER.md section 1: the reference host answers // "recording":true from a Start() that set the flag false. So every test here // checks the REPORTED state against what is actually on disk, not against what // the call returned. diff --git a/tests/test_staleness.cpp b/tests/test_staleness.cpp index 43213b1..b7ad00d 100644 --- a/tests/test_staleness.cpp +++ b/tests/test_staleness.cpp @@ -3,7 +3,7 @@ // The obvious black-box test - freeze the process and re-query - is WORTHLESS: // SIGSTOP freezes the HTTP server too, so the poller refreshes the cache the // instant the process resumes and the answer comes back fresh. It looks like a -// pass and measures nothing. That is the same failure shape as CARRYOVER.md +// pass and measures nothing. That is the same failure shape as docs/internal/CARRYOVER.md // section 3's byte-count latency estimate, which read ~0 in steady state while // 435ms sat in the ALSA buffer. // diff --git a/tests/test_watchdog.cpp b/tests/test_watchdog.cpp index bba8937..b5f5a03 100644 --- a/tests/test_watchdog.cpp +++ b/tests/test_watchdog.cpp @@ -2,7 +2,7 @@ // // The watchdog is the safety property in the whole host: if it does not fire, // a lost link or a slept laptop leaves the rig keyed with nobody watching -// (CARRYOVER.md section 4b). So the test asserts the RADIO actually stopped +// (docs/internal/CARRYOVER.md section 4b). So the test asserts the RADIO actually stopped // transmitting, read back through CAT - not merely that a trip counter moved. // A counter is a claim; TX; returning 0 is the outcome. diff --git a/tools/__pycache__/parity_check.cpython-312.pyc b/tools/__pycache__/parity_check.cpython-312.pyc index 8169537cf2dc64a9b2654b07e24d24e97a18e570..983c6fb4f71ba466c73b0fe24a9dec5ef444e274 100644 GIT binary patch delta 27 icmX@?chPUdK_kBdf delta 12 TcmccUci3;kL8i^;nVM7pDoO>; diff --git a/tools/backup.sh b/tools/backup.sh index 2d94211..daa2b82 100755 --- a/tools/backup.sh +++ b/tools/backup.sh @@ -7,7 +7,7 @@ # ⚠️ THE BUNDLE CONTAINS THE FULL HISTORY, INCLUDING SITE DETAIL that has been # removed from the working tree but is still in older commits. It is safe on # private storage and MUST NOT be published. That is also why this script has no -# GitHub path in it - see the push blocker in WIP.md. +# GitHub path in it - see the push blocker in docs/internal/WIP.md. # # No host or path is baked in: this repo is public. # HAMDECK_BACKUP_HOST= HAMDECK_BACKUP_PATH= tools/backup.sh diff --git a/tools/parity_check.py b/tools/parity_check.py index d0bbe92..98a4754 100755 --- a/tools/parity_check.py +++ b/tools/parity_check.py @@ -8,7 +8,7 @@ A parity walker that simply GETs every route would key the transmitter, change the operating mode and retune the amplifier on a live station - most of this API -is state-changing, and many of those routes are GETs. CARRYOVER.md section 9 +is state-changing, and many of those routes are GETs. docs/internal/CARRYOVER.md section 9 records that probing with a control route once changed the operating mode mid-session, with a human at the radio. diff --git a/tools/walk_all_routes.py b/tools/walk_all_routes.py index 5d7da9f..2e23f88 100755 --- a/tools/walk_all_routes.py +++ b/tools/walk_all_routes.py @@ -5,7 +5,7 @@ ⚠️ THIS TOOL FIRES STATE-CHANGING ROUTES. It will key PTT, change mode, switch antennas and move the frequency. Against a real station that is exactly the -accident CARRYOVER.md section 9 records - a control-route probe that changed the +accident docs/internal/CARRYOVER.md section 9 records - a control-route probe that changed the operating mode mid-session. So it refuses to run unless the target PROVES it is a simulator: From 27a00b94c3b53e19a406d091b8cb5d2b94f619df Mon Sep 17 00:00:00 2001 From: wa0o Date: Wed, 2 Sep 2026 16:02:39 +0000 Subject: [PATCH 2/2] Protect main the moment the repo can be protected GitHub warns that main is unprotected, and the API answers 403 "Upgrade to GitHub Pro or make this repository public" to both rulesets and classic branch protection. So the warning is not actionable while the repo is private - it becomes free the instant it goes public, which is a bad thing to leave as something somebody is supposed to remember afterwards. The ruleset requires the three CI checks, blocks force-push and deletion of main, and requires a pull request with ZERO required approvals - a solo maintainer cannot approve their own PR, so requiring one would lock the repository against its only committer while buying nothing. The repository admin can bypass, so a broken CI config can never leave you unable to fix your own repo. Run .github/apply-main-ruleset.sh at the moment of flipping to public. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EqubrbjLXwz1GrVgmTcrf9 --- .github/apply-main-ruleset.sh | 23 +++++++++++++++++++++++ .github/main-ruleset.json | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100755 .github/apply-main-ruleset.sh create mode 100644 .github/main-ruleset.json diff --git a/.github/apply-main-ruleset.sh b/.github/apply-main-ruleset.sh new file mode 100755 index 0000000..1fc28fb --- /dev/null +++ b/.github/apply-main-ruleset.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Protect main. ⚠️ ONLY WORKS ON A PUBLIC REPO (or GitHub Pro) - the API answers +# 403 "Upgrade to GitHub Pro or make this repository public" otherwise, which is +# why this is a script to run at the moment of flipping rather than a setting +# somebody is supposed to remember afterwards. +# +# What it does and does NOT do, deliberately: +# * requires the three CI checks to pass - the gates are the point +# * blocks force-push and deletion of main - the two irreversible ones +# * requires a pull request, with ZERO approvals - a solo maintainer cannot +# approve their own PR, so requiring one would lock the repo against its +# only committer. The PR requirement still buys the CI gate and a diff to +# read before merging. +# * lets the repo ADMIN bypass - so a broken CI config can +# never leave you unable to fix your own repository. +set -euo pipefail +REPO="${1:-jwussler/hamdeck-cpp}" +cd "$(dirname "$0")" +echo "applying the main ruleset to $REPO" +gh api --method POST "repos/$REPO/rulesets" --input main-ruleset.json \ + --jq '"created ruleset \(.id): \(.name) (\(.enforcement))"' +echo "verifying it is actually there:" +gh api "repos/$REPO/rulesets" --jq '.[] | " \(.name) \(.enforcement)"' diff --git a/.github/main-ruleset.json b/.github/main-ruleset.json new file mode 100644 index 0000000..786f6e8 --- /dev/null +++ b/.github/main-ruleset.json @@ -0,0 +1,34 @@ +{ + "name": "main", + "target": "branch", + "enforcement": "active", + "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } }, + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "required_status_checks": [ + { "context": "build-and-run" }, + { "context": "macos-universal" }, + { "context": "pusher" } + ] + } + } + ], + "bypass_actors": [ + { "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "always" } + ] +}