From 1a425a1b0dfe6b7bfc9a72c2a4f7c111ea11fade Mon Sep 17 00:00:00 2001 From: rouggy Date: Wed, 3 Jun 2026 21:53:31 +0200 Subject: [PATCH] bug --- app.go | 58 +++++++++-- frontend/src/App.tsx | 69 +++++++++--- frontend/src/components/BandMap.tsx | 2 +- frontend/src/components/QSOContextMenu.tsx | 29 +++++- frontend/src/components/QSOEditModal.tsx | 41 ++++---- frontend/src/components/RecentQSOsGrid.tsx | 25 +++-- frontend/src/components/SettingsModal.tsx | 12 +-- frontend/src/components/WorkedBeforeGrid.tsx | 4 +- frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 + internal/adif/import.go | 9 ++ internal/cat/log.go | 42 +++++--- internal/cat/omnirig.go | 104 ++++++++++++++----- internal/extsvc/manager.go | 56 ++++++++++ internal/extsvc/qrz.go | 17 ++- 15 files changed, 377 insertions(+), 97 deletions(-) diff --git a/app.go b/app.go index bfa0e1d..4831c79 100644 --- a/app.go +++ b/app.go @@ -448,6 +448,9 @@ func (a *App) startup(ctx context.Context) { if _, err := applog.Init(dataDir); err != nil { fmt.Println("OpsLog: log init:", err) } + // Route CAT/OmniRig debug lines into the unified app log (they used to go + // to a separate cat.log in the old HamLog folder, which users couldn't find). + cat.LogSink = applog.Printf applog.Printf("startup: data dir = %s", dataDir) conn, err := db.Open(a.dbPath) if err != nil { @@ -544,11 +547,12 @@ func (a *App) startup(ctx context.Context) { // from settings and host callbacks to build ADIF, stamp the upload // status and surface errors to the UI. a.extsvc = extsvc.NewManager(extsvc.Deps{ - BuildADIF: a.buildUploadADIF, - MarkUploaded: a.markExtUploaded, - NotifyError: a.notifyExtError, - ShouldUpload: a.extShouldUpload, - Logf: applog.Printf, + BuildADIF: a.buildUploadADIF, + MarkUploaded: a.markExtUploaded, + NotifyError: a.notifyExtError, + ShouldUpload: a.extShouldUpload, + StationCallOf: a.stationCallOf, + Logf: applog.Printf, }) a.extsvc.SetConfig(a.loadExternalServices()) @@ -998,6 +1002,13 @@ func (a *App) ListCountries() []string { return a.dxcc.EntityNames() } +// DXCCForCountry returns the ADIF DXCC entity number for a country/entity +// name (as listed by ListCountries), or 0 if unknown. The QSO editor uses it +// to keep the read-only DXCC field in sync when the user picks a Country. +func (a *App) DXCCForCountry(name string) int { + return dxcc.EntityDXCC(name) +} + // ComputeStationInfo resolves a station's structured metadata from the // callsign (via cty.dat) and grid (via Maidenhead → lat/lon). The // frontend calls this whenever Callsign or Grid changes in the Station @@ -2424,6 +2435,19 @@ func (a *App) buildUploadADIF(id int64, forceCall string) (string, bool) { return adif.SingleRecordADIF(q), true } +// stationCallOf returns the QSO's STATION_CALLSIGN (upper-cased), used by the +// uploader to verify a QSO belongs to the target logbook's callsign. +func (a *App) stationCallOf(id int64) string { + if a.qso == nil { + return "" + } + q, err := a.qso.GetByID(a.ctx, id) + if err != nil { + return "" + } + return strings.ToUpper(strings.TrimSpace(q.StationCallsign)) +} + // extShouldUpload reports whether a QSO is eligible for upload to a service, // based on its sent status. QRZ/Club Log upload anything not yet "Y"; LoTW // uploads only QSOs whose lotw_sent matches the configured Upload flag @@ -2438,9 +2462,17 @@ func (a *App) extShouldUpload(svc extsvc.Service, id int64) bool { } switch svc { case extsvc.ServiceQRZ: - return !strings.EqualFold(q.QRZComUploadStatus, "Y") + if strings.EqualFold(q.QRZComUploadStatus, "Y") { + applog.Printf("extsvc: QSO %d not eligible for qrz — QRZComUploadStatus already %q (set Confirmations default to N to upload)", id, q.QRZComUploadStatus) + return false + } + return true case extsvc.ServiceClublog: - return !strings.EqualFold(q.ClublogUploadStatus, "Y") + if strings.EqualFold(q.ClublogUploadStatus, "Y") { + applog.Printf("extsvc: QSO %d not eligible for clublog — ClublogUploadStatus already %q (set Confirmations default to N to upload)", id, q.ClublogUploadStatus) + return false + } + return true case extsvc.ServiceLoTW: flag := "R" if a.settings != nil { @@ -2952,7 +2984,11 @@ func (a *App) SetCATFrequency(hz int64) error { if a.cat == nil { return fmt.Errorf("cat not initialized") } - return a.cat.SetFrequency(hz) + err := a.cat.SetFrequency(hz) + if err != nil { + applog.Printf("cat: SetFrequency(%d Hz) dispatch error: %v", hz, err) + } + return err } // SetCATMode sets the rig's mode. ADIF mode names (SSB / CW / FT8 / …) are @@ -2961,7 +2997,11 @@ func (a *App) SetCATMode(mode string) error { if a.cat == nil { return fmt.Errorf("cat not initialized") } - return a.cat.SetMode(mode) + err := a.cat.SetMode(mode) + if err != nil { + applog.Printf("cat: SetMode(%q) dispatch error: %v", mode, err) + } + return err } // SwitchCATRig hot-swaps the active OmniRig slot (Rig1 ↔ Rig2) without diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2e458f1..c706309 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,7 +8,7 @@ import { AddQSO, ListQSO, CountQSO, OpenADIFFile, ImportADIF, SaveADIFFile, ExportADIF, GetQSO, UpdateQSO, DeleteQSO, DeleteAllQSO, - UpdateQSOsFromCty, UpdateQSOsFromQRZ, + UpdateQSOsFromCty, UpdateQSOsFromQRZ, UploadQSOsManual, LookupCallsign, GetStationSettings, GetListsSettings, GetStartupStatus, WorkedBefore, @@ -555,6 +555,18 @@ export default function App() { const [pendingImportPath, setPendingImportPath] = useState(null); const [importDupMode, setImportDupMode] = useState<'skip' | 'update' | 'all'>('skip'); const [importApplyCty, setImportApplyCty] = useState(true); + // QRZ profile photo lightbox (full-size, in-app — not the browser). + const [photoModal, setPhotoModal] = useState(null); + // Esc closes the lightbox. Capture-phase + stopImmediatePropagation so the + // global ESC handler (which resets the entry) doesn't also fire. + useEffect(() => { + if (!photoModal) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { e.stopImmediatePropagation(); e.preventDefault(); setPhotoModal(null); } + }; + window.addEventListener('keydown', onKey, true); + return () => window.removeEventListener('keydown', onKey, true); + }, [photoModal]); // === Lookup + WB === const [lookupResult, setLookupResult] = useState(null); @@ -1083,6 +1095,16 @@ export default function App() { try { await afterBulkUpdate(await UpdateQSOsFromQRZ(ids as any), 'from QRZ.com'); } catch (e: any) { setError(String(e?.message ?? e)); } } + // Right-click "Send to QRZ.com / Club Log / LoTW": uploads the selected QSOs + // on demand (regardless of their current upload status). Runs in the + // background; qslmgr:done refreshes the grid when finished. + async function bulkSendTo(service: string, ids: number[]) { + if (ids.length === 0) return; + const label = service === 'qrz' ? 'QRZ.com' : service === 'clublog' ? 'Club Log' : service === 'lotw' ? 'LoTW' : service; + showToast(`Uploading ${ids.length} QSO${ids.length > 1 ? 's' : ''} to ${label}…`); + try { await UploadQSOsManual(service, ids as any); } + catch (e: any) { setError(String(e?.message ?? e)); } + } function askDelete(id: number) { const q = qsos.find((x) => x.id === id); if (q) setDeletingQSO(q); @@ -1273,9 +1295,6 @@ export default function App() { { name: 'tools', label: 'Tools', items: [ { type: 'item', label: 'QSL Manager…', action: 'tools.qslmanager' }, { type: 'separator' }, - { type: 'item', label: 'Callsign lookup settings…', action: 'tools.lookup' }, - { type: 'item', label: 'CAT interface…', action: 'tools.cat' }, - { type: 'item', label: 'Rotator…', action: 'tools.rotator' }, { type: 'item', label: wkEnabled ? '✓ WinKeyer CW keyer' : 'WinKeyer CW keyer', action: 'tools.winkeyer' }, { type: 'separator' }, // Maintenance — bumped here while we only have one entry. Will move @@ -1298,9 +1317,6 @@ export default function App() { case 'edit.delete': if (selectedId !== null) askDelete(selectedId); break; case 'edit.prefs': setShowSettings(true); break; case 'tools.qslmanager': setQslTabOpen(true); setActiveTab('qsl'); break; - case 'tools.lookup': setSettingsSection('lookup'); setShowSettings(true); break; - case 'tools.cat': setSettingsSection('cat'); setShowSettings(true); break; - case 'tools.rotator': setSettingsSection('rotator'); setShowSettings(true); break; case 'tools.winkeyer': wkSetEnabled(!wkEnabled); break; case 'tools.refreshCty': refreshCtyDat(); break; } @@ -1389,7 +1405,10 @@ export default function App() { tabIndex={-1} onClick={() => { const c = callsign.trim().toUpperCase(); - OpenExternalURL(`https://www.qrz.com/db/${encodeURIComponent(c)}`) + // Encode each segment but keep the '/' literal — QRZ's URL is + // /db/5Z4/MM0ZBH, not /db/5Z4%2FMM0ZBH (which 404s). + const path = c.split('/').map(encodeURIComponent).join('/'); + OpenExternalURL(`https://www.qrz.com/db/${path}`) .catch((err) => setError(String(err?.message ?? err))); }} title="Open this callsign on QRZ.com" @@ -1800,6 +1819,31 @@ export default function App() { )} + {/* QRZ profile photo lightbox — full size, in-app. Click anywhere or + press Esc to close; click the image itself doesn't close. */} + {photoModal && ( +
setPhotoModal(null)} + > + + profile full size e.stopPropagation()} + /> +
+ )} + {/* Transient success toast (bottom-right). */} {toast && (
@@ -1966,9 +2010,9 @@ export default function App() {
+ + {onSendTo && ( + <> +
+ {UPLOAD_TARGETS.map((t) => ( + + ))} + + )}
); } diff --git a/frontend/src/components/QSOEditModal.tsx b/frontend/src/components/QSOEditModal.tsx index 9d32bd6..95cb66b 100644 --- a/frontend/src/components/QSOEditModal.tsx +++ b/frontend/src/components/QSOEditModal.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { Trash2, Search, Loader2 } from 'lucide-react'; -import { LookupCallsign } from '../../wailsjs/go/main/App'; +import { LookupCallsign, DXCCForCountry } from '../../wailsjs/go/main/App'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from '@/components/ui/dialog'; @@ -14,6 +14,7 @@ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; +import { Combobox } from '@/components/ui/combobox'; import { cn } from '@/lib/utils'; import { flagURL } from '@/lib/flags'; import type { QSOForm } from '@/types'; @@ -63,6 +64,11 @@ const CONFIRMATIONS: ConfDef[] = [ // Colour-coded status cell for the confirmation grid. function StatusCell({ value }: { value?: string }) { const v = (value || '').toUpperCase(); + // Empty = no value set yet → show a neutral dash, NOT "No" (which is the + // explicit "N" status). Mirrors the dropdown, which shows "—" for empty. + if (v === '') { + return ; + } const label = v === 'Y' ? 'Yes' : v === 'R' ? 'Requested' : v === 'I' ? 'Ignore' : v === 'M' ? 'Modified' : 'No'; const cls = v === 'Y' ? 'bg-emerald-600 text-white' : v === 'R' ? 'bg-orange-400 text-white' @@ -76,6 +82,7 @@ interface Props { onSave: (q: QSO) => void; onDelete: (id: number) => void; onClose: () => void; + countries?: string[]; } function toLocalISO(d: any): string { @@ -138,7 +145,7 @@ function QslSelect({ value, onChange }: { value?: string; onChange: (v: string) ); } -export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) { +export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [] }: Props) { const [draft, setDraft] = useState(() => JSON.parse(JSON.stringify(qso))); // Frequencies are edited as kHz + Hz (Log4OM style) and recombined on save. const splitHz = (hz?: number) => hz @@ -163,6 +170,16 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) { setDraft((d) => ({ ...d, [key]: value })); } + // Country drives the DXCC entity number (ADIF). The DXCC field is read-only; + // picking a Country resolves and stamps its DXCC# so they can't diverge. + async function onCountryChange(v: string) { + set('country', v); + try { + const n = await DXCCForCountry(v); + set('dxcc', (n && n > 0 ? n : undefined) as any); + } catch { /* leave DXCC as-is if resolution fails */ } + } + // Re-run a callsign lookup (QRZ/HamQTH + cty.dat) and merge the result into // the draft — handy after correcting the callsign. Only overwrites the // lookup-derived fields; leaves call/band/mode/RST/dates alone. @@ -270,7 +287,6 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) { Contest Sat / Prop My Station - Notes Extras {extrasCount > 0 && ( @@ -331,14 +347,15 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) {
- set('country', e.target.value)} className="flex-1" /> +
set('ituz', intOrUndef(e.target.value) as any)} className="font-mono w-16 text-center" /> set('cqz', intOrUndef(e.target.value) as any)} className="font-mono w-16 text-center" /> - set('dxcc', intOrUndef(e.target.value) as any)} className="font-mono w-16 text-center bg-muted/40" title="DXCC entity #" /> + {flagURL(draft.dxcc) && }
@@ -368,11 +385,6 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) {
set('comment', e.target.value)} />