up
This commit is contained in:
+10
-1
@@ -4,7 +4,16 @@
|
|||||||
"Bash(go get *)",
|
"Bash(go get *)",
|
||||||
"Bash(go build *)",
|
"Bash(go build *)",
|
||||||
"Bash(wails generate *)",
|
"Bash(wails generate *)",
|
||||||
"Bash(npm run *)"
|
"Bash(npm run *)",
|
||||||
|
"Bash(gofmt -w app.go internal/winkeyer/winkeyer.go)",
|
||||||
|
"Bash(timeout 20 git status --short)",
|
||||||
|
"Bash(echo \"exit: $?\")",
|
||||||
|
"Bash(GIT_TERMINAL_PROMPT=0 timeout 20 git push --dry-run)",
|
||||||
|
"Bash(echo \"push exit: $?\")",
|
||||||
|
"Bash(git config *)",
|
||||||
|
"Bash(ls \"/c/Program Files/Git/mingw64/bin/git-credential-manager\"*.exe)",
|
||||||
|
"Bash(ls \"/c/Program Files/Git/mingw64/libexec/git-core/git-credential-manager\"*.exe)",
|
||||||
|
"Bash(which git-credential-manager *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -1194,10 +1195,20 @@ func (a *App) reloadLookupProviders() {
|
|||||||
|
|
||||||
// --- QSO bindings ---
|
// --- QSO bindings ---
|
||||||
|
|
||||||
func (a *App) AddQSO(q qso.QSO) (int64, error) {
|
func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
|
||||||
if a.qso == nil {
|
if a.qso == nil {
|
||||||
return 0, fmt.Errorf("db not initialized")
|
return 0, fmt.Errorf("db not initialized")
|
||||||
}
|
}
|
||||||
|
// Never let a panic in the logging path crash the whole app — the user lost
|
||||||
|
// QSOs that way (CW + WinKeyer). Surface it as an error instead.
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
applog.Printf("PANIC in AddQSO: %v\n%s", r, debug.Stack())
|
||||||
|
if id == 0 {
|
||||||
|
err = fmt.Errorf("internal error while logging: %v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
a.applyStationDefaults(&q)
|
a.applyStationDefaults(&q)
|
||||||
a.applyDXCCNumber(&q)
|
a.applyDXCCNumber(&q)
|
||||||
a.applyClublogException(&q, false) // override entity for date-ranged DXpeditions
|
a.applyClublogException(&q, false) // override entity for date-ranged DXpeditions
|
||||||
@@ -1210,7 +1221,7 @@ func (a *App) AddQSO(q qso.QSO) (int64, error) {
|
|||||||
q.Email = lr.Email
|
q.Email = lr.Email
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
id, err := a.qso.Add(a.ctx, q)
|
id, err = a.qso.Add(a.ctx, q)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
q.ID = id
|
q.ID = id
|
||||||
a.saveQSORecording(&q)
|
a.saveQSORecording(&q)
|
||||||
|
|||||||
+16
-6
@@ -1294,9 +1294,15 @@ export default function App() {
|
|||||||
email: details.email,
|
email: details.email,
|
||||||
};
|
};
|
||||||
applyAwardRefs(payload, details.award_refs ?? '', awardFieldRef.current);
|
applyAwardRefs(payload, details.award_refs ?? '', awardFieldRef.current);
|
||||||
|
const loggedCall = String(payload.callsign ?? '');
|
||||||
|
const loggedDxcc = typeof payload.dxcc === 'number' ? payload.dxcc : 0;
|
||||||
await AddQSO(payload);
|
await AddQSO(payload);
|
||||||
resetEntry();
|
resetEntry();
|
||||||
await refresh();
|
await refresh();
|
||||||
|
// Refresh the Worked-before matrix so the just-logged band/mode flips to
|
||||||
|
// "worked" — resetEntry cleared it, so re-fetch for the logged call (a
|
||||||
|
// live DB query, so it now includes this QSO).
|
||||||
|
if (loggedCall.length >= 3) runWorkedBefore(loggedCall, loggedDxcc);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(String(e?.message ?? e));
|
setError(String(e?.message ?? e));
|
||||||
} finally { setSaving(false); }
|
} finally { setSaving(false); }
|
||||||
@@ -1556,6 +1562,11 @@ export default function App() {
|
|||||||
setDetails((d) => ({ ...d, award_refs: refs.map((r) => `POTA@${r}`).join(';') }));
|
setDetails((d) => ({ ...d, award_refs: refs.map((r) => `POTA@${r}`).join(';') }));
|
||||||
}
|
}
|
||||||
function onCallsignInput(v: string, opts?: { force?: boolean }) {
|
function onCallsignInput(v: string, opts?: { force?: boolean }) {
|
||||||
|
// Programmatic call-sets (force: spot click, UDP, external app) count as
|
||||||
|
// "not manually typed", so a later UDP DX call (DXHunter remote control) can
|
||||||
|
// still replace it. Without this, clicking a cluster spot froze the call:
|
||||||
|
// applyUdpCall saw current != lastUdpCall and refused every later UDP call.
|
||||||
|
if (opts?.force) lastUdpCallRef.current = v.trim().toUpperCase();
|
||||||
// No-op guard: external apps (MSHV/WSJT-X) re-broadcast the same DX call
|
// No-op guard: external apps (MSHV/WSJT-X) re-broadcast the same DX call
|
||||||
// on every status packet. If it matches what's already in the entry,
|
// on every status packet. If it matches what's already in the entry,
|
||||||
// do nothing — otherwise we'd re-run the QRZ lookup, hit the cache and
|
// do nothing — otherwise we'd re-run the QRZ lookup, hit the cache and
|
||||||
@@ -1567,13 +1578,12 @@ export default function App() {
|
|||||||
// Recording START happens on blur (leaving the callsign field), NOT here —
|
// Recording START happens on blur (leaving the callsign field), NOT here —
|
||||||
// you may type a call and work it minutes later. Clearing it cancels.
|
// you may type a call and work it minutes later. Clearing it cancels.
|
||||||
if (v.trim() === '') { QSOAudioCancel(); setRecording(false); recordingCallRef.current = ""; }
|
if (v.trim() === '') { QSOAudioCancel(); setRecording(false); recordingCallRef.current = ""; }
|
||||||
const wasEmpty = callsign.trim() === '';
|
|
||||||
const isEmpty = v.trim() === '';
|
const isEmpty = v.trim() === '';
|
||||||
if (wasEmpty && !isEmpty && !locks.start) {
|
if (!isEmpty && !locks.start) {
|
||||||
// First keystroke of a new QSO — freeze the start time so it doesn't
|
// Restart the start time on every callsign change (each keystroke, a
|
||||||
// drift even if the lookup or typing takes 30 seconds. Skip when
|
// clicked spot, a new call): "Start UTC" should mark when you actually
|
||||||
// start is locked: the user is back-entering a past QSO and set a
|
// began this contact, not a stale time frozen at the first keystroke.
|
||||||
// specific time manually.
|
// Skip when start is locked (back-entering a past QSO at a chosen time).
|
||||||
setQsoStartedAt(new Date());
|
setQsoStartedAt(new Date());
|
||||||
} else if (isEmpty && !locks.start) {
|
} else if (isEmpty && !locks.start) {
|
||||||
// Callsign wiped → user abandoned this QSO; reset the timer.
|
// Callsign wiped → user abandoned this QSO; reset the timer.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ package winkeyer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -197,9 +198,11 @@ func (m *Manager) applyConfig(c Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// modeRegister builds the WinKey mode-register byte (command 0x0E).
|
// modeRegister builds the WinKey mode-register byte (command 0x0E).
|
||||||
|
//
|
||||||
// bits 1..0 : paddle mode (00 Iambic-B, 01 Iambic-A, 10 Ultimatic, 11 Bug)
|
// bits 1..0 : paddle mode (00 Iambic-B, 01 Iambic-A, 10 Ultimatic, 11 Bug)
|
||||||
// bit 3 : paddle swap
|
// bit 3 : paddle swap
|
||||||
// bit 0/... : (autospace is bit 0 of a separate group on some firmwares)
|
// bit 0/... : (autospace is bit 0 of a separate group on some firmwares)
|
||||||
|
//
|
||||||
// We keep to the widely-compatible WK2 layout.
|
// We keep to the widely-compatible WK2 layout.
|
||||||
func modeRegister(c Config) byte {
|
func modeRegister(c Config) byte {
|
||||||
var b byte
|
var b byte
|
||||||
@@ -339,6 +342,14 @@ func (m *Manager) Disconnect() {
|
|||||||
// the echo of characters being sent. We track busy/idle and the speed pot.
|
// the echo of characters being sent. We track busy/idle and the speed pot.
|
||||||
func (m *Manager) readLoop(p serial.Port, stop, done chan struct{}) {
|
func (m *Manager) readLoop(p serial.Port, stop, done chan struct{}) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
// A panic here (e.g. in a status/echo callback) would otherwise take down
|
||||||
|
// the whole app — which showed up as a crash when logging a CW QSO while
|
||||||
|
// the keyer was still streaming echo bytes. Recover, log, end the loop.
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
applog.Printf("winkeyer: readLoop panic recovered: %v\n%s", r, debug.Stack())
|
||||||
|
}
|
||||||
|
}()
|
||||||
buf := make([]byte, 64)
|
buf := make([]byte, 64)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|||||||
Reference in New Issue
Block a user