Files
OpsLog/main.go
T
rouggy cc1ad06a9d fix: keep the precise locator on digital QSOs; make the manual lookup real
Grid on the UDP path — WSJT-X and MSHV can only send a FOUR-character grid,
that is all the FT8 protocol carries. The enrichment rule there is "fill only
what is empty", so the coarse JN05 always won and QRZ's JN05JG was thrown away:
roughly 100 km of accuracy discarded on every digital QSO. The lookup grid is
now taken when it EXTENDS the received square. A lookup that disagrees outright
(JN18 against JN05) is not a refinement — the station may be portable and what
came over the air is then the better record. Both rules are pinned by tests.

Manual fetch in the QSO editor — it read the CACHE, valid for 30 days. An
operator who upgraded their QRZ subscription went on getting the thin
free-account record and clearing the cached row by hand was the only way out.
The button now forces a lookup that bypasses the cache, reaches the provider and
REFRESHES the stored row, with a 15 s budget instead of 2 s: a deliberate click
must reach the network, where giving up early would fall back to cty.dat and
look like nothing happened. The automatic type-ahead lookup keeps the cache,
which is where it earns its keep.

Same fetch: it used `??`, which only guards against null. Go marshals an unset
string as "", so a QRZ record with no grid BLANKED the grid already in the QSO.
The lookup still wins — that is the point of asking for it — but an empty
result no longer erases a good value.
2026-07-27 22:10:00 +02:00

145 lines
4.8 KiB
Go

package main
import (
"embed"
"os"
"strings"
"time"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
var assets embed.FS
// profileArg extracts a profile name from the command line. Accepts
// "--profile NAME", "--profile=NAME", "-profile NAME", "-p NAME" so a desktop
// shortcut can launch OpsLog straight into a given profile (e.g. F4BPO / TM2Q).
func profileArg(args []string) string {
for i := 0; i < len(args); i++ {
a := args[i]
switch {
case a == "--profile" || a == "-profile" || a == "-p":
if i+1 < len(args) {
return strings.TrimSpace(args[i+1])
}
case strings.HasPrefix(a, "--profile="):
return strings.TrimSpace(strings.TrimPrefix(a, "--profile="))
case strings.HasPrefix(a, "-profile="):
return strings.TrimSpace(strings.TrimPrefix(a, "-profile="))
case strings.HasPrefix(a, "-p="):
return strings.TrimSpace(strings.TrimPrefix(a, "-p="))
}
}
return ""
}
// hasFlag reports whether flag is present in args.
func hasFlag(args []string, flag string) bool {
for _, a := range args {
if a == flag {
return true
}
}
return false
}
// acquireInstance grabs the single-instance mutex. On a normal launch it's a plain
// try (fail → another OpsLog is running, so exit). On a --post-update relaunch the
// previous instance may still be shutting down and holding the mutex, so retry for
// a few seconds until it frees.
func acquireInstance(postUpdate bool) bool {
if acquireSingleInstance() {
return true
}
if !postUpdate {
return false
}
deadline := time.Now().Add(20 * time.Second)
for time.Now().Before(deadline) {
time.Sleep(300 * time.Millisecond)
if acquireSingleInstance() {
return true
}
}
return false
}
func main() {
// Single-instance guard: if OpsLog is already running, focus that window and
// exit instead of spawning a duplicate. A second process would open its own
// CAT (FlexRadio) connection and Ultrabeam follow loop, and the two would
// fight over the rig/antenna frequency — the cause of "the antenna re-tunes on
// its own" when a windowless zombie instance was left running.
// A --post-update relaunch (from the auto-updater) may start while the previous
// instance is still exiting and holding the single-instance mutex — wait for it
// to free instead of bailing out. Then clear the old exe it left behind.
postUpdate := hasFlag(os.Args[1:], "--post-update")
if !acquireInstance(postUpdate) {
return
}
if postUpdate {
cleanupOldUpdateBinary()
}
// Create an instance of the app structure
app := NewApp()
app.startupProfile = profileArg(os.Args[1:])
// Restore the window's SIZE and maximised state at CREATION, from the geometry
// saved on last close. Doing it here (not after startup) is what makes the
// window open already at the right size instead of maximising then snapping
// smaller. Position can't be set through options, so it is applied while the
// window is still hidden (domReady) — invisible, so no jump. First run, or a
// window closed maximised, keeps the historical maximised default.
width, height := 1400, 900
startState := options.Maximised
if dataDir, err := userDataDir(); err == nil {
if ws, ok := readWindowState(dataDir); ok && !ws.Maximised &&
ws.Width >= 1100 && ws.Height >= 700 && ws.Width <= 8000 && ws.Height <= 6000 {
width, height = ws.Width, ws.Height
startState = options.Normal
}
}
// Create application with options
err := wails.Run(&options.App{
Title: "OpsLog",
Width: width,
Height: height,
MinWidth: 1100,
MinHeight: 700,
WindowStartState: startState,
// No OS title bar: it was a dead 32-pixel band above a window that already
// has its own title strip. The app header takes over — it carries the drag
// region, the double-click-to-maximise, and the minimise/maximise/close
// buttons. Windows still draws the resize borders and honours Aero Snap,
// which is why the frame is dropped rather than the whole chrome.
Frameless: true,
// Start hidden and reveal only once the saved position has been applied and
// the DOM has painted (OnDomReady → domReady) — so the window appears
// already at its final size and position, with no post-launch jump.
StartHidden: true,
AssetServer: &assetserver.Options{
Assets: assets,
},
// The colour the window is painted before the WebView draws. Matches the
// graphite theme's background (#16181d), which is the default on a fresh
// install — otherwise every launch flashes white first.
BackgroundColour: &options.RGBA{R: 0x16, G: 0x18, B: 0x1d, A: 1},
OnStartup: app.startup,
OnDomReady: app.domReady,
OnBeforeClose: app.beforeClose,
OnShutdown: app.shutdown,
Bind: []interface{}{
app,
},
})
if err != nil {
println("Error:", err.Error())
}
}