diff --git a/app.go b/app.go index 9bad169..6abeb4a 100644 --- a/app.go +++ b/app.go @@ -6096,37 +6096,31 @@ func (a *App) extShouldUpload(svc extsvc.Service, id int64) bool { // tells the frontend to refresh that row's confirmation columns. func (a *App) markExtUploaded(svc extsvc.Service, id int64, logID string) { date := time.Now().UTC().Format("20060102") + // Use a fresh background context, NOT a.ctx: this stamp often runs during + // the on-close upload, and a.ctx is cancelled as the app shuts down — which + // would silently abort the UPDATE and leave the QSO at "R" forever despite a + // successful upload. + ctx := context.Background() + if a.qso == nil { + return + } + var err error switch svc { case extsvc.ServiceQRZ: - if a.qso != nil { - if err := a.qso.MarkQRZUploaded(a.ctx, id, date); err != nil { - applog.Printf("extsvc: mark qrz uploaded %d: %v", id, err) - } - } + err = a.qso.MarkQRZUploaded(ctx, id, date) case extsvc.ServiceClublog: - if a.qso != nil { - if err := a.qso.MarkClublogUploaded(a.ctx, id, date); err != nil { - applog.Printf("extsvc: mark clublog uploaded %d: %v", id, err) - } - } + err = a.qso.MarkClublogUploaded(ctx, id, date) case extsvc.ServiceLoTW: - if a.qso != nil { - if err := a.qso.MarkLoTWUploaded(a.ctx, id, date); err != nil { - applog.Printf("extsvc: mark lotw uploaded %d: %v", id, err) - } - } + err = a.qso.MarkLoTWUploaded(ctx, id, date) case extsvc.ServiceHRDLog: - if a.qso != nil { - if err := a.qso.MarkHRDLogUploaded(a.ctx, id, date); err != nil { - applog.Printf("extsvc: mark hrdlog uploaded %d: %v", id, err) - } - } + err = a.qso.MarkHRDLogUploaded(ctx, id, date) case extsvc.ServiceEQSL: - if a.qso != nil { - if err := a.qso.MarkEQSLSent(a.ctx, id, date); err != nil { - applog.Printf("extsvc: mark eqsl sent %d: %v", id, err) - } - } + err = a.qso.MarkEQSLSent(ctx, id, date) + } + if err != nil { + applog.Printf("extsvc: mark %s uploaded %d failed: %v", svc, id, err) + } else { + applog.Printf("extsvc: marked %s QSO %d as sent", svc, id) } if a.ctx != nil { wruntime.EventsEmit(a.ctx, "extsvc:uploaded", map[string]any{ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 237385a..03b65da 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -597,6 +597,9 @@ export default function App() { const [cwText, setCwText] = useState(''); const [cwStatus, setCwStatus] = useState<{ wpm: number; pitch: number; level: number; active: boolean }>({ wpm: 0, pitch: 0, level: 0, active: false }); const cwOn = cwEnabled && mode === 'CW'; + // Keep the decoded line scrolled to the newest text (left-aligned, no scrollbar). + const cwScrollRef = useRef(null); + useEffect(() => { const el = cwScrollRef.current; if (el) el.scrollLeft = el.scrollWidth; }, [cwText]); useEffect(() => { const offT = EventsOn('cw:text', (t: string) => setCwText((s) => (s + t).slice(-200))); const offS = EventsOn('cw:status', (st: any) => setCwStatus(st)); @@ -3202,7 +3205,7 @@ export default function App() { {/* ===== CW decoder strip (only when enabled AND mode is CW) ===== */} {cwOn && ( -
+
{/* Input-level meter — if this stays flat with a strong signal, the RX audio device is wrong/silent rather than a decode problem. */} @@ -3212,18 +3215,18 @@ export default function App() { {cwStatus.wpm > 0 ? `${cwStatus.wpm} WPM` : '— WPM'} · {cwStatus.pitch > 0 ? `${cwStatus.pitch} Hz` : '— Hz'} - {/* Single-line rolling ticker — no scrollbar; newest text stays - pinned to the right, older text scrolls off the left. */} -
+ {/* Left-aligned single line, no scrollbar; auto-scrolled to the newest + text (see cwScrollRef effect) so the latest stays in view. */} +
{cwText.trim() === '' ? ( listening… ) : ( -
+
{cwText.trim().split(/\s+/).map((tok, i) => (