fix: log returns as soon as the QSO row is inserted; award_refs/recording/uploads/eQSL now run off the critical path (both manual and UDP) — a busy multi-op MySQL no longer freezes the entry form ~40s waiting on the award_refs UPDATE

This commit is contained in:
2026-07-21 16:20:49 +02:00
parent ea1bd2a66d
commit aa537f9eea
+37 -22
View File
@@ -1948,25 +1948,36 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
}
if err == nil {
q.ID = id
a.noteWorked(q.Callsign, q.Band, q.Mode) // keep the alert worked-index fresh
a.noteLiveQSO() // multi-op: flip this operator back "online"
a.materializeAwardRefs(q) // stamp award_refs so the grid columns show at once
// Announce the log so UI widgets can react (e.g. the Flex panel zeroing RIT).
a.noteWorked(q.Callsign, q.Band, q.Mode) // keep the alert worked-index fresh (in-memory)
a.noteLiveQSO() // multi-op: flip this operator back "online" (publishes async)
// Announce the log RIGHT AWAY so the grid/UI refresh at once and the entry
// form clears immediately — the operator is not made to wait on the DB.
wruntime.EventsEmit(a.ctx, "qso:logged", id)
a.saveQSORecording(&q)
// Everything below writes to (or reads from) the shared DB. Run it OFF the
// critical path: on a busy multi-op MySQL the award_refs UPDATE alone could
// block ~40s (and hit "Too many connections"), which used to freeze the log
// (form stuck, "…" on the button) until it finished. The QSO row is already
// safely inserted; these just enrich it, so they can lag a beat.
qc := q
go func() {
a.materializeAwardRefs(qc) // fills the award_refs column
a.saveQSORecording(&qc)
if a.extsvc != nil {
a.extsvc.OnQSOLogged(id)
}
a.maybeAutoSendEQSL(q)
// Forward the ADIF of this QSO to any outbound "ADIF message" UDP rows.
a.maybeAutoSendEQSL(qc)
if a.udp != nil {
go a.udp.EmitLoggedADIF(adif.SingleRecordADIF(q))
a.udp.EmitLoggedADIF(adif.SingleRecordADIF(qc))
}
// A successful write means the link is healthy again — if QSOs are still
// parked, get them in now rather than waiting for the next tick.
// Nudge the grid again so the award_refs columns appear once materialised.
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "qso:logged", id)
}
// A successful write means the link is healthy — flush any parked QSOs.
if a.offlineMode && a.offlineQ != nil && a.offlineQ.Count() > 0 {
go func() { _, _ = a.replayOfflineQueue() }()
_, _ = a.replayOfflineQueue()
}
}()
}
return id, err
}
@@ -9634,24 +9645,28 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
return 0, fmt.Errorf("insert qso: %w", err)
}
q.ID = id
a.noteLiveQSO() // multi-op: flip this operator back "online"
a.materializeAwardRefs(q)
// Announce the log so UI widgets refresh AT ONCE (the Recent-QSOs grid, the
// ON-AIR badge and the "stations on air" widget). The manual log path always
// did this; the UDP/FT8 path did not, so a station running MSHV saw the top
// "on air" list lag ~15-45s behind the instant bottom badge.
a.noteLiveQSO() // multi-op: flip this operator back "online" (publishes async)
// Announce the log AT ONCE so the grid / ON-AIR badge / stations-on-air widget
// refresh immediately, then run the DB-heavy enrichment off the critical path
// (same reasoning as the manual path award_refs on a busy shared MySQL blocked).
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "qso:logged", id)
}
a.saveQSORecording(&q)
qc := q
go func() {
a.materializeAwardRefs(qc)
a.saveQSORecording(&qc)
if a.extsvc != nil {
a.extsvc.OnQSOLogged(id)
}
a.maybeAutoSendEQSL(q)
// A successful write means the link is healthy again — flush anything parked.
if a.offlineMode && a.offlineQ != nil && a.offlineQ.Count() > 0 {
go func() { _, _ = a.replayOfflineQueue() }()
a.maybeAutoSendEQSL(qc)
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "qso:logged", id) // refresh again so award_refs show
}
if a.offlineMode && a.offlineQ != nil && a.offlineQ.Count() > 0 {
_, _ = a.replayOfflineQueue()
}
}()
return id, nil
}