110 lines
8.9 KiB
Markdown
110 lines
8.9 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## What this is
|
|
|
|
OpsLog is a Windows ham-radio logger built with **Wails v2** — Go backend, React/TypeScript frontend, compiled into a single `.exe`. Author F4BPO. See `README.md` for the user-facing feature list and `wiki/` for end-user documentation.
|
|
|
|
**Pure Go, no CGO** — SQLite is `modernc.org/sqlite`, serial is `go.bug.st/serial`. Any dependency requiring cgo breaks the build; check before adding one.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
wails dev # hot-reload dev (Go methods also reachable at http://localhost:34115)
|
|
wails build # full build → build/bin/OpsLog.exe (~25 s)
|
|
wails generate module # REQUIRED after changing exported App methods (regenerates TS bindings)
|
|
|
|
go build ./... # fast Go-only check
|
|
go test ./... # all Go tests
|
|
go test ./internal/steppir/ -run TestApplyPendingDirHold -v # one test
|
|
gofmt -w <file>
|
|
|
|
cd frontend && npx tsc --noEmit # frontend typecheck alone (wails build does this too)
|
|
```
|
|
|
|
Release: `.vscode/release.ps1` (Ctrl+Shift+P → *Tasks: Run Task* → *Release OpsLog*) — bumps the version, pushes to Gitea, builds and publishes to Gitea + GitHub.
|
|
|
|
Prefer `wails build` for final validation: it is the only command that exercises Go, the TS typecheck, the bindings and the asset embed together.
|
|
|
|
## Navigating app.go
|
|
|
|
`app.go` is ~14 000 lines and holds nearly every Wails binding. **Do not read it linearly** — it is organised into banner-delimited sections:
|
|
|
|
```bash
|
|
grep -n "^// ──\|^// ---" app.go # table of contents
|
|
```
|
|
|
|
Sections map to features (`── Motorized antenna (Ultrabeam / SteppIR) ──`, `── NET Control ──`, `── DX-cluster spot alerts ──`, …). Find the banner, then read that range. The `App` struct (~line 448) is the other useful landmark: its fields document every subsystem and carry substantial explanatory comments.
|
|
|
|
Smaller root files split off self-contained areas: `app_cw.go`, `app_qsl_designer.go`, `app_secret.go`, `chat.go`, `offline.go`, `relayauto.go`, `adifwatch.go`, `livestatus.go`, `update.go`, `telemetry.go`.
|
|
|
|
## Architecture
|
|
|
|
### Wails binding boundary
|
|
|
|
Exported methods on `*App` are the entire frontend API. Adding or changing a signature requires `wails generate module`, which writes `frontend/wailsjs/go/main/App.d.ts` and `models.ts`. Parameters and return types must be Wails-serializable — the generator prints `Not found: time.Time` noise for unsupported types (that particular message is long-standing and harmless).
|
|
|
|
Backend→frontend push uses Wails events (`runtime.EventsEmit` / `EventsOn`), e.g. `qso:logged`, `update:progress`. Most hardware status is **polled** by the frontend on an interval rather than pushed.
|
|
|
|
### Two separate databases
|
|
|
|
This split is easy to get wrong and matters:
|
|
|
|
- **`a.db`** — settings database (`settings.db` / `opslog.db`). Always SQLite. Holds settings and profiles. Its location is chosen by the user and recorded in `data/config.json`.
|
|
- **`a.logDb`** — the logbook (QSOs). Either a per-profile SQLite file, the default `logbook.db`, or a **shared MySQL** so several operators log into one database. `a.dbBackend` says which.
|
|
|
|
QSOs never go to `a.db`. `internal/db` holds the connection helpers and the MySQL specifics; `internal/qso` is the repository.
|
|
|
|
Remote MySQL is the slow path: startup deliberately brings CAT/rig links up **before** connecting the logbook, and hot paths (cluster spot enrichment, alert matching) avoid per-row queries — hence the in-memory `wcbm` worked-index and the `clusterEvents` queue that keeps the telnet socket draining.
|
|
|
|
### Per-profile settings scoping
|
|
|
|
`internal/settings.Store` is a key/value store over the settings DB. Every key is transparently prefixed with the active profile (`p3.`), so each station profile has a complete independent set. Two consequences:
|
|
|
|
- `App.settingsScoped` (atomic) gates reads until the active profile is known. Anything reading settings early must respect it or it reads the wrong scope.
|
|
- Settings keys are declared as `key<Thing>` string constants near the top of `app.go` (~180 of them) — grep `key[A-Z].*= "` to find one.
|
|
|
|
Password-type keys are encrypted at rest when the secret vault is unlocked (`internal/secret`); a locked vault returns `""` rather than ciphertext, and callers treat that as "not configured".
|
|
|
|
### Hardware device pattern
|
|
|
|
The codebase talks to ~20 devices (rigs, amplifiers, antenna controllers, switches, keyers, rotators). They all follow the same shape — match it when adding one:
|
|
|
|
1. **`internal/<device>/`** — a self-contained package exposing a `Client` with `New(...)`, `Start()`, `Stop()`, `GetStatus()`. The client owns its own goroutine: a reconnecting poll loop, a mutex serialising the shared connection, and a cached last-known `Status`. Transports are usually TCP *or* serial behind one `io.ReadWriteCloser`.
|
|
2. **Settings** — `key<Thing>*` constants plus `Get<Thing>Settings()` / `Save<Thing>Settings()` bindings in `app.go`.
|
|
3. **Lifecycle** — a `start<Thing>()` method that tears down any existing client and rebuilds it from settings; called at startup and again whenever settings are saved.
|
|
4. **Status binding** — `Get<Thing>Status()`, polled by the frontend.
|
|
5. **UI** — a settings panel in `frontend/src/components/SettingsModal.tsx` and a live widget in `frontend/src/components/`.
|
|
|
|
`internal/steppir` and `internal/antgenius` are compact, well-commented references. Wire-protocol packages carry the byte layout in the package doc comment and pin it with table tests against **real captured frames** — keep that up, since a wrong byte silently mistunes an antenna.
|
|
|
|
Where two devices are interchangeable (Ultrabeam / SteppIR), `app.go` defines a small interface (`motorAntenna`) plus thin per-device adapters rather than branching everywhere.
|
|
|
|
### CAT
|
|
|
|
`internal/cat` (~7 000 lines, the largest package) is the rig abstraction: OmniRig, native FlexRadio/SmartSDR, native Icom CI-V (USB **and** remote-over-internet), and TCI (SunSDR / Expert Electronics). `cat.Manager` exposes a backend-agnostic `State()`; backend-specific features are reached through typed escapes such as `FlexDo(func(cat.FlexController) error)`. Check `State().Backend` before using one.
|
|
|
|
### Logging
|
|
|
|
`internal/applog` — Wails builds with the Windows GUI subsystem, so `fmt.Println` is discarded. Use `applog.Printf` (or plain `log.Printf` inside `internal/` packages) so output reaches the rotating log file in the user data dir. That file is usually the only evidence available when diagnosing a user's hardware problem.
|
|
|
|
## Conventions
|
|
|
|
**Changelog is mandatory.** Every user-visible change gets an entry in `changelog.json`, **in both `en` and `fr`**, in the same session as the change. Keep entries to one or two sentences — rationale belongs in the commit message. New work goes under the next version number; the release script bumps the version constants.
|
|
|
|
**Version lives in two places** and must stay in lockstep: `appVersion` in `telemetry.go` and `APP_VERSION` in `frontend/src/version.ts`.
|
|
|
|
**Bilingual UI.** Every user-visible string goes through `t()` from `frontend/src/lib/i18n.tsx` (~700 keys), with both English and French provided. No hardcoded display strings.
|
|
|
|
**Commit messages must not include a `Co-Authored-By` line or any mention of the model.**
|
|
|
|
**Comments explain *why*, not *what*.** The existing code documents the reasoning behind non-obvious decisions — which protocol source was trusted, what field failure a workaround addresses, why an ordering matters. Match that: a comment that restates the code is noise, a comment recording the hard-won reason is why this codebase is navigable.
|
|
|
|
## Gotchas
|
|
|
|
- **Adding a promoted ADIF field touches five places in lockstep.** `internal/adif` promotes ~30 ADIF fields to real QSO columns; miss one and imports silently drop data. All of: the struct field, column list, insert args and scan in `internal/qso/qso.go`; the dictionary entry (`Promoted: true`) in `internal/adif/fields.go`; the `adifPromoted` list *and* the assignment in `internal/adif/import.go`; the writer in `internal/adif/export.go`; then the frontend column in `RecentQSOsGrid.tsx` with its EN+FR i18n keys. Trace an existing field (e.g. `ant_path`) across the repo as a template.
|
|
- **Generated files — don't hand-edit.** `internal/dxcc/dxcc_names_gen.go` (cty.dat joined to the ARRL/ADIF entity list) and `internal/awardref/uscounties_gen.go` (emitted by `cmd/cntygen` from the FIPS county CSV; a one-shot generator, not part of the build).
|
|
- **Single-instance guard** (`main.go`): a second process would open its own CAT and antenna-follow loops and the two would fight over the rig frequency.
|
|
- **Frontend controlled inputs**: several editors normalise a value on every keystroke (trim, split, filter). Binding an input directly to the normalised form makes Enter/Space appear dead — keep raw text in local state and derive the stored value from it.
|