From 3dd9620ccac285139ad50593dcc90ff7579ca27e Mon Sep 17 00:00:00 2001 From: rouggy Date: Sun, 7 Jun 2026 17:45:08 +0200 Subject: [PATCH] up --- OpsLog-res.syso | Bin 34740 -> 0 bytes app.go | 48 +++++++++++-- frontend/src/App.tsx | 55 +++++++++++---- frontend/src/components/ClusterGrid.tsx | 86 +++++++++--------------- frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 ++ internal/audio/recorder.go | 15 +++++ 7 files changed, 133 insertions(+), 77 deletions(-) delete mode 100644 OpsLog-res.syso diff --git a/OpsLog-res.syso b/OpsLog-res.syso deleted file mode 100644 index 79a098913b91d83ef9f5a4aa1c1e459865bb119f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34740 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 Wzf&wu&Q7L(W`o}-zbX8FG5mj31gjwc diff --git a/app.go b/app.go index 07c7507..41839b0 100644 --- a/app.go +++ b/app.go @@ -3457,6 +3457,16 @@ func (a *App) QSOAudioBegin() bool { return a.qsoRec.Active() } +// QSOAudioRestart starts a fresh recording for a new target even if one is +// already in progress (new call+freq from a clicked spot or external app). +func (a *App) QSOAudioRestart() bool { + if a.qsoRec == nil { + return false + } + a.qsoRec.RestartQSO() + return a.qsoRec.Active() +} + // QSOAudioCancel drops the in-progress recording (callsign cleared, QSO // abandoned without logging). func (a *App) QSOAudioCancel() { @@ -4587,6 +4597,7 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe // that count for ARRL awards) so each incoming one is flagged NEW. sets, _ := a.qso.ConfirmedSlots(ctx, []string{"lotw_rcvd", "qsl_rcvd"}) var items []ConfirmationItem + var unmatched []string perr := adif.Parse(strings.NewReader(adifText), func(rec adif.Record) error { q, ok := adif.RecordToQSO(rec) if !ok { @@ -4611,6 +4622,12 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe keyIDs[key] = newID // guard against dup records in the report added++ } + } else { + // No local QSO matched this confirmation on (call, minute, band, + // mode). Record the specifics so the user can see WHICH one and + // why (time off by a minute, FT4 logged as MFSK, portable call…). + unmatched = append(unmatched, fmt.Sprintf("%s · %s · %s · %s", + q.Callsign, q.QSODate.UTC().Format("2006-01-02 15:04Z"), q.Band, q.Mode)) } // Build the result row + NEW flags (vs the pre-download snapshot), // then fold this slot into the sets so a repeat in the same batch @@ -4648,6 +4665,12 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe } else { emit(fmt.Sprintf("Matched %d of %d confirmed QSO(s)", matched, total)) } + // Surface confirmations with no local match so the user sees WHICH one + // and why (time off by a minute, FT4 logged as MFSK, portable call, or + // never logged). Tick "Add not-found" to import them instead. + for _, u := range unmatched { + emit(" ⚠ no local QSO for: " + u) + } // Remember today so the next pull is incremental (per active profile). if a.settings != nil { _ = a.settings.Set(ctx, a.profileScope()+keyExtLoTWLastDownload, time.Now().UTC().Format("2006-01-02")) @@ -4658,10 +4681,11 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe // and (when a window is requested) skip records older than sinceDate by // QSO date. sinceDate is "YYYY-MM-DD". sinceDate := resolveSince(keyExtQRZLastDownload) + emit(fmt.Sprintf("Window: since=%q → resolved date=%q (key %s%s)", since, sinceDate, a.profileScope(), keyExtQRZLastDownload)) if sinceDate != "" { - emit("Fetching QRZ.com logbook (QSOs since " + sinceDate + ")…") + emit("Fetching QRZ.com logbook (will skip QSOs before " + sinceDate + ")…") } else { - emit("Fetching QRZ.com logbook…") + emit("Fetching QRZ.com logbook (full — no since date)…") } fr, err := extsvc.FetchQRZ(ctx, nil, cfg.QRZ.APIKey, "ALL") if err != nil { @@ -4671,6 +4695,14 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe } adifText := fr.ADIF emit(fmt.Sprintf("QRZ RESULT=%s COUNT=%s, ADIF %d bytes", fr.Result, fr.Count, len(adifText))) + // Persist the last-download date NOW (right after a successful fetch), + // not at the end: the QRZ logbook can be huge (tens of thousands of + // records) and the user may close the panel mid-processing — storing it + // late meant the date was never saved, so "since last download" kept + // resolving to empty and re-pulled everything. + if a.settings != nil { + _ = a.settings.Set(ctx, a.profileScope()+keyExtQRZLastDownload, time.Now().UTC().Format("2006-01-02")) + } if snip := strings.TrimSpace(adifText); snip != "" { if len(snip) > 300 { snip = snip[:300] @@ -4781,10 +4813,7 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe sort.Strings(keys) emit(fmt.Sprintf("Parsed %d record(s). Fields seen: %s", parsed, strings.Join(keys, ", "))) emit(fmt.Sprintf("Confirmed %d, added %d (of %d returned)", matched, added, total)) - // Remember today so a later "since last download" pull is incremental. - if a.settings != nil { - _ = a.settings.Set(ctx, a.profileScope()+keyExtQRZLastDownload, time.Now().UTC().Format("2006-01-02")) - } + // (last-download date already stored right after the fetch above) default: emit(fmt.Sprintf("Confirmation download isn't available for %s yet.", svc)) @@ -6296,7 +6325,12 @@ func (a *App) GetWinkeyerSettings() (WinkeyerSettings, error) { out.AutoSpace = v == "1" } out.UsePTT = m[keyWKUsePTT] == "1" - out.SerialEcho = m[keyWKSerialEcho] == "1" + // Only override the default (true) when the key is actually stored — otherwise + // settings saved before serial_echo existed would silently disable the echo, + // and the TX text would stop showing as it's keyed. + if v := m[keyWKSerialEcho]; v != "" { + out.SerialEcho = v == "1" + } if v := m[keyWKMacros]; v != "" { var mac []WKMacro if json.Unmarshal([]byte(v), &mac) == nil && len(mac) > 0 { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 20d26d2..9493bad 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -27,7 +27,7 @@ import { GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts, GetWinkeyerStatus, WinkeyerConnect, WinkeyerDisconnect, WinkeyerSend, WinkeyerStop, WinkeyerSetSpeed, WinkeyerBackspace, GetDVKMessages, GetDVKStatus, DVKPlay, DVKStop, - QSOAudioBegin, QSOAudioCancel, + QSOAudioBegin, QSOAudioCancel, QSOAudioRestart, GetAwardDefs, } from '../wailsjs/go/main/App'; import { Combobox } from '@/components/ui/combobox'; @@ -654,6 +654,11 @@ export default function App() { // tell whether an incoming DX call actually changed anything. const callsignValRef = useRef(''); useEffect(() => { callsignValRef.current = callsign; }, [callsign]); + // True while the operator is typing in the Call field. A call change that + // arrives while it's NOT focused is programmatic (clicked spot / external app + // via UDP) → we (re)start the recording immediately; typed changes wait for + // blur so we don't restart on every keystroke. + const callFocusedRef = useRef(false); // When the entered callsign turns out to be worked-before, jump to the // Worked-before tab so the history is front-and-centre. Only once per call, @@ -817,6 +822,16 @@ export default function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // applyModeFromSpot updates the mode AND its RST default for a fresh target + // (clicked spot / rig-driven mode change). Unlike a manual mode tweak, this + // is a new contact, so we clear the "user edited RST" flag first — otherwise + // a 599 left from a CW QSO would stick when jumping to an SSB spot. + function applyModeFromSpot(m: string) { + if (!m) return; + setMode(m); + rstUserEditedRef.current = false; + applyModePreset(m); + } function applyModePreset(m: string) { if (rstUserEditedRef.current) return; // Prefer the user's configured preset RST; otherwise fall back to the mode @@ -876,12 +891,15 @@ export default function App() { // 3. Else trust CAT (SSB, CW, AM, FM…). if (!lk.mode) { const inferred = s.freq_hz ? inferDigitalMode(s.freq_hz) : ''; - if (inferred) { - setMode(inferred); - } else if (s.mode === 'DATA') { - setMode(digitalDefaultRef.current || 'FT8'); - } else if (s.mode) { - setMode(s.mode); + let nextMode = ''; + if (inferred) nextMode = inferred; + else if (s.mode === 'DATA') nextMode = digitalDefaultRef.current || 'FT8'; + else if (s.mode) nextMode = s.mode; + if (nextMode) { + setMode(nextMode); + // Flip the RST default (599↔59) when the rig changes mode. Respects a + // user-edited RST (applyModePreset early-returns when edited). + applyModePreset(nextMode); } } }); @@ -1394,6 +1412,13 @@ export default function App() { } setCallsign(v); scheduleLookup(v); + // Programmatic call change (clicked spot, or external app via UDP) for a new + // non-empty target → (re)start the recording now, even if one was already + // running for the previous contact. Typed changes (field focused) wait for + // blur so we don't restart per keystroke. + if (v.trim() !== '' && !callFocusedRef.current) { + QSOAudioRestart().then(setRecording).catch(() => {}); + } } function markEdited(field: string) { userEditedRef.current.add(field); } @@ -1615,10 +1640,11 @@ export default function App() { ref={callsignRef} className="font-mono text-base font-bold tracking-wider uppercase h-9 bg-muted/40 focus:bg-card" value={callsign} + onFocus={() => { callFocusedRef.current = true; }} onChange={(e) => onCallsignInput(e.target.value)} // Start the QSO recording when leaving the callsign field (the pre-roll // covers the seconds before). No-op when the recorder is off. - onBlur={() => { if (callsign.trim()) QSOAudioBegin().then(setRecording).catch(() => {}); }} + onBlur={() => { callFocusedRef.current = false; if (callsign.trim()) QSOAudioBegin().then(setRecording).catch(() => {}); }} /> @@ -2585,17 +2611,18 @@ export default function App() { } else { setFreqMhz((s.freq_hz / 1_000_000).toFixed(5)); if (s.band) setBand(s.band); - if (m) setMode(m); } + // Set mode + flip the RST default (599↔59) for the new + // target — a plain setMode skipped the RST preset. + if (m) applyModeFromSpot(m); onCallsignInput(s.dx_call); // A POTA spot carries the park ref — pre-fill the POTA // award reference (like the State→RAC auto-match) so it's // logged without re-typing. n-fer refs (comma-separated) // become one POTA@ entry each. applySpotPOTA((s as any).pota_ref); - // Clicking a spot fills the call programmatically (no blur - // on the call field), so start the QSO recording here too. - if (s.dx_call.trim()) QSOAudioBegin().then(setRecording).catch(() => {}); + // (recording (re)starts inside onCallsignInput — the call + // changed programmatically with the field unfocused.) }} /> ); @@ -2808,11 +2835,11 @@ export default function App() { } else { setFreqMhz((s.freq_hz / 1_000_000).toFixed(5)); if (s.band) setBand(s.band); - if (m) setMode(m); } + if (m) applyModeFromSpot(m); onCallsignInput(s.dx_call); applySpotPOTA((s as any).pota_ref); - if (s.dx_call.trim()) QSOAudioBegin().then(setRecording).catch(() => {}); + // (recording (re)starts inside onCallsignInput — programmatic call change) }} onClose={() => setShowBandMap(false)} /> diff --git a/frontend/src/components/ClusterGrid.tsx b/frontend/src/components/ClusterGrid.tsx index a7079fa..88a9593 100644 --- a/frontend/src/components/ClusterGrid.tsx +++ b/frontend/src/components/ClusterGrid.tsx @@ -105,6 +105,14 @@ function fmtDateTimeUTC(s: any): string { type ColEntry = ColDef & { group: string; label: string; defaultVisible?: boolean }; +// statusFor resolves the precomputed spot status (new / new-band / new-slot / +// worked-call) for an ag-Grid cell's row. +function statusFor(p: any): SpotStatusEntry | undefined { + return p?.context?.spotStatus?.[ + spotStatusKey(p.data?.dx_call, p.data?.band ?? '', p.data?.comment ?? '', p.data?.freq_hz) + ]; +} + const COL_CATALOG: ColEntry[] = [ { group: 'Spot', label: 'Time', colId: 'time', @@ -117,28 +125,15 @@ const COL_CATALOG: ColEntry[] = [ group: 'Spot', label: 'Call', colId: 'call', headerName: 'Call', field: 'dx_call' as any, width: 120, defaultVisible: true, - cellRenderer: (p: any) => { - if (!p.value) return ''; - const status: SpotStatusEntry | undefined = p.context?.spotStatus?.[ - spotStatusKey(p.data.dx_call, p.data.band ?? '', p.data.comment ?? '', p.data.freq_hz) - ]; - const isNew = status?.status === 'new'; - const workedCall = !!status?.worked_call; - const style: any = { - fontFamily: 'ui-monospace, monospace', fontWeight: 700, fontSize: 12, - }; - if (isNew) { - // New DXCC entity — soft rose pill, no clashing border. - style.backgroundColor = '#ffe4e6'; - style.color = '#be123c'; - style.padding = '1px 7px'; - style.borderRadius = 4; - } else if (workedCall) { - style.color = '#0369a1'; // already worked this exact call - } else { - style.color = '#b8410c'; // new call in a worked entity - } - return {p.value}; + cellClass: 'font-mono', + // New DXCC entity → fill the whole cell (no padded pill, so calls stay + // aligned with non-new rows). Text colour also flags worked-call vs new-call. + cellStyle: (p: any): any => (statusFor(p)?.status === 'new' + ? { backgroundColor: '#ffe4e6', color: '#be123c', fontWeight: 700 } + : { color: statusFor(p)?.worked_call ? '#0369a1' : '#b8410c', fontWeight: 700 }), + tooltipValueGetter: (p: any) => { + const s = statusFor(p); + return s?.status === 'new' ? `NEW DXCC: ${s?.country ?? ''}` : s?.worked_call ? 'Already worked this call' : undefined; }, }, { @@ -159,46 +154,25 @@ const COL_CATALOG: ColEntry[] = [ group: 'Spot', label: 'Band', colId: 'band', headerName: 'Band', field: 'band' as any, width: 75, defaultVisible: true, - cellClass: 'flex items-center', - cellRenderer: (p: any) => { - const status: SpotStatusEntry | undefined = p.context?.spotStatus?.[ - spotStatusKey(p.data.dx_call, p.data.band ?? '', p.data.comment ?? '', p.data.freq_hz) - ]; - const newBand = status?.status === 'new-band'; - return p.value - ? {p.value} - : ''; - }, + cellClass: 'font-mono', + // NEW BAND for this entity → fill the cell (keeps the band text aligned). + cellStyle: (p: any) => (statusFor(p)?.status === 'new-band' + ? { backgroundColor: '#fde68a', color: '#92400e', fontWeight: 700 } + : undefined), + tooltipValueGetter: (p: any) => (statusFor(p)?.status === 'new-band' ? 'NEW BAND for this entity' : undefined), }, { group: 'Spot', label: 'Mode', colId: 'mode', headerName: 'Mode', colSpan: undefined, width: 80, defaultVisible: true, - cellClass: 'flex items-center', + cellClass: 'font-mono', valueGetter: (p: any) => p.data ? inferSpotMode(p.data.comment ?? '', p.data.freq_hz) : '', - cellRenderer: (p: any) => { - const status: SpotStatusEntry | undefined = p.context?.spotStatus?.[ - spotStatusKey(p.data.dx_call, p.data.band ?? '', p.data.comment ?? '', p.data.freq_hz) - ]; - const newSlot = status?.status === 'new-slot'; - return p.value - ? {p.value} - : ; - }, + // NEW SLOT (mode not yet worked on this band) → fill the cell. + cellStyle: (p: any) => (statusFor(p)?.status === 'new-slot' + ? { backgroundColor: '#fef08a', color: '#854d0e', fontWeight: 700 } + : undefined), + cellRenderer: (p: any) => p.value ? p.value : , + tooltipValueGetter: (p: any) => (statusFor(p)?.status === 'new-slot' ? 'NEW SLOT (mode not yet worked on this band)' : undefined), }, { group: 'Spot', label: 'Pfx', colId: 'pfx', diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 70e1372..fbd1626 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -237,6 +237,8 @@ export function QSOAudioBegin():Promise; export function QSOAudioCancel():Promise; +export function QSOAudioRestart():Promise; + export function QuitApp():Promise; export function RefreshCtyDat():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index f93a170..a2d47f0 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -446,6 +446,10 @@ export function QSOAudioCancel() { return window['go']['main']['App']['QSOAudioCancel'](); } +export function QSOAudioRestart() { + return window['go']['main']['App']['QSOAudioRestart'](); +} + export function QuitApp() { return window['go']['main']['App']['QuitApp'](); } diff --git a/internal/audio/recorder.go b/internal/audio/recorder.go index 1aa2e98..bbdd1eb 100644 --- a/internal/audio/recorder.go +++ b/internal/audio/recorder.go @@ -206,6 +206,21 @@ func (r *Recorder) BeginQSO() { r.active = true } +// RestartQSO begins a fresh accumulation even if one is already active — +// re-seeding from the pre-roll ring. Used when the target QSO changes (a new +// call+freq from a clicked spot or an external app) so the previous take is +// dropped and a new one starts from the pre-roll, rather than continuing to +// accumulate the old contact. +func (r *Recorder) RestartQSO() { + r.mu.Lock() + defer r.mu.Unlock() + if !r.running { + return + } + r.acc = append([]int16(nil), r.ring...) + r.active = true +} + // SaveQSO writes the accumulated recording to path as a WAV and stops // accumulating. Returns an error if no recording was active. func (r *Recorder) SaveQSO(path string) error {