chore: release v0.21.3
This commit is contained in:
@@ -793,6 +793,14 @@ func (a *App) startup(ctx context.Context) {
|
||||
cat.LogSink = applog.Printf
|
||||
audio.LogSink = applog.Printf // capture audio-goroutine panics in the app log
|
||||
extsvc.LogSink = applog.Printf // log raw QRZ (and other) service responses for diagnosis
|
||||
lookup.LogSink = applog.Printf // which call was queried, and why a portable lookup fell back
|
||||
db.LogSink = applog.Printf // which schema migrations ran, and how long they took
|
||||
// Version and executable FIRST, before anything else can fail. Diagnosing a
|
||||
// report means knowing which build produced the log, and that was previously
|
||||
// impossible: the copy someone is actually running is not always the one they
|
||||
// think they installed, and the version shown in the UI is the only clue.
|
||||
exe, _ := os.Executable()
|
||||
applog.Printf("startup: OpsLog %s — %s", appVersion, exe)
|
||||
applog.Printf("startup: data dir = %s", dataDir)
|
||||
// The local SQLite file ALWAYS holds per-operator configuration — settings,
|
||||
// station profiles, rigs/antennas, cluster nodes, UDP, QSL templates, award
|
||||
@@ -1568,9 +1576,44 @@ func (a *App) restoreWindowPosition() {
|
||||
if ws.Width < normalMinW || ws.Height < normalMinH || ws.Width > maxW || ws.Height > maxH {
|
||||
return // corrupt / absurd — leave the default placement
|
||||
}
|
||||
// The SIZE was sanity-checked above but the POSITION never was, and that is
|
||||
// the one that makes OpsLog unusable: close it on a second monitor, unplug
|
||||
// that monitor (or dock elsewhere, or change the layout), and the saved
|
||||
// coordinates put the window somewhere no screen covers. It opens, invisibly,
|
||||
// forever — with no way back short of deleting window.json, which nobody
|
||||
// knows to do. Fall back to the default placement instead.
|
||||
if !onSomeMonitor(ws.X, ws.Y, ws.Width, ws.Height) {
|
||||
applog.Printf("window: saved position %d,%d (%dx%d) is off every monitor — opening at the default placement",
|
||||
ws.X, ws.Y, ws.Width, ws.Height)
|
||||
return
|
||||
}
|
||||
wruntime.WindowSetPosition(a.ctx, ws.X, ws.Y)
|
||||
}
|
||||
|
||||
// onSomeMonitor reports whether a window at these coordinates would land on the
|
||||
// visible desktop. It demands a real slab of the title bar rather than a single
|
||||
// pixel: a window overlapping the screen edge by 2 px is, in practice, as lost
|
||||
// as one entirely outside it. When the desktop bounds can't be read, it says yes
|
||||
// — better to honour the operator's saved position than to second-guess it.
|
||||
func onSomeMonitor(x, y, w, h int) bool {
|
||||
vx, vy, vw, vh, ok := virtualScreenBounds()
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return overlapsEnough(x, y, w, h, vx, vy, vw, vh)
|
||||
}
|
||||
|
||||
// overlapsEnough is the geometry behind onSomeMonitor, split out so it can be
|
||||
// tested — the virtual desktop origin is NEGATIVE when a monitor sits left of or
|
||||
// above the primary one, which is precisely the layout that produces a lost
|
||||
// window and precisely where sign errors hide.
|
||||
func overlapsEnough(x, y, w, h, vx, vy, vw, vh int) bool {
|
||||
const grabW, grabH = 160, 32 // enough of the title bar to see and drag
|
||||
overlapW := min(x+w, vx+vw) - max(x, vx)
|
||||
overlapH := min(y+h, vy+vh) - max(y, vy)
|
||||
return overlapW >= grabW && overlapH >= grabH
|
||||
}
|
||||
|
||||
// readBootstrap returns the full bootstrap config (DB path + MySQL), or a zero
|
||||
// value if the file is missing/unreadable.
|
||||
func readBootstrap(dataDir string) dbPointer {
|
||||
@@ -5472,6 +5515,20 @@ func (a *App) BulkUpdateField(ids []int64, field, value string) (int64, error) {
|
||||
if field == "freq" {
|
||||
return a.bulkSetFrequency(ids, value)
|
||||
}
|
||||
// Some ADIF fields have no promoted column and live in extras_json
|
||||
// (OWNER_CALLSIGN) — those take the JSON path so the rest of the extras on
|
||||
// each QSO survive the edit.
|
||||
if key := qso.BulkExtraKey(field); key != "" {
|
||||
n, err := a.qso.BulkSetExtra(a.ctx, ids, key, strings.TrimSpace(value))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n > 0 {
|
||||
a.invalidateAwardStats()
|
||||
a.materializeAwardRefsForIDs(ids)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
col, ok := bulkFieldColumns[field]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unknown field %q", field)
|
||||
@@ -6076,12 +6133,23 @@ func (a *App) LookupCallsign(callsign string) (lookup.Result, error) {
|
||||
if a.lookup == nil {
|
||||
return lookup.Result{}, fmt.Errorf("lookup not initialized")
|
||||
}
|
||||
// Bound the whole lookup: give the providers a couple of seconds, then let
|
||||
// Lookup fall through to cty.dat (country/zones). Without this a call that isn't
|
||||
// in QRZ.com — or a slow/unresponsive provider — left the "looking up" spinner
|
||||
// turning for 10 s+ before the cty.dat fallback showed. The providers respect
|
||||
// the context, so they're cancelled at the deadline and cty.dat answers instantly.
|
||||
ctx, cancel := context.WithTimeout(a.ctx, 2*time.Second)
|
||||
// Bound the whole lookup, then let Lookup fall through to cty.dat
|
||||
// (country/zones). Without this a call that isn't in QRZ.com — or a slow
|
||||
// provider — left the "looking up" spinner turning for 10 s+ before the
|
||||
// cty.dat fallback showed. The providers respect the context, so they're
|
||||
// cancelled at the deadline and cty.dat answers instantly.
|
||||
//
|
||||
// A slashed/portable call needs a bigger slice, because it costs TWO provider
|
||||
// round trips rather than one: the full form is looked up first, and only when
|
||||
// that comes back not-found does the home call get tried (F4LYI/M → F4LYI).
|
||||
// Two seconds covered one request but not two, so /M and /P calls always ran
|
||||
// out of time on the second and fell back to cty.dat — even though the
|
||||
// operator's QRZ record was there and the plain call resolved fine.
|
||||
budget := 2 * time.Second
|
||||
if strings.Contains(callsign, "/") {
|
||||
budget = 6 * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(a.ctx, budget)
|
||||
defer cancel()
|
||||
r, err := a.lookup.Lookup(ctx, callsign)
|
||||
if errors.Is(err, lookup.ErrNotFound) {
|
||||
@@ -8023,7 +8091,7 @@ func (a *App) pttKey(cfg AudioSettings) error {
|
||||
a.pttKeyedMethod = "cat"
|
||||
a.pttGen++
|
||||
a.pttMu.Unlock()
|
||||
applog.Printf("dvk: PTT keyed (CAT/OmniRig)")
|
||||
applog.Printf("dvk: PTT keyed (CAT via %s)", a.cat.State().Backend)
|
||||
return nil
|
||||
case "rts", "dtr":
|
||||
if strings.TrimSpace(cfg.PTTPort) == "" {
|
||||
@@ -8212,6 +8280,19 @@ func (a *App) GetLogFilePath() string {
|
||||
return applog.Path()
|
||||
}
|
||||
|
||||
// UILog lets the frontend write to the same diagnostic log as the backend.
|
||||
//
|
||||
// Frontend-only logic (entry-strip auto-fill, debounce ordering, which field the
|
||||
// operator has touched) was previously invisible in a bug report: console output
|
||||
// dies with the window, and these problems reproduce on other operators'
|
||||
// machines, not here. A line in opslog.log can simply be sent along with the
|
||||
// report. Callers prefix their own subsystem, e.g. "backfill: …".
|
||||
func (a *App) UILog(msg string) {
|
||||
if msg = strings.TrimSpace(msg); msg != "" {
|
||||
applog.Printf("ui: %s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// ── QSL defaults ──────────────────────────────────────────────────────
|
||||
|
||||
// GetQSLDefaults returns the stored defaults — empty strings when the
|
||||
|
||||
Reference in New Issue
Block a user