feat+fix batch: Station Control dashboard (fixed-width panels, grip-handle drag reorder, amp meters from the Flex stream, compact relays), Ultrabeam element list capped to 3 (READ_BANDS over-read), spot-click on Flex panadapter now sets mode, DAX TX toggle, DVK panel state persists across relaunch, duplicate amp meters cleared on power-cycle, QSO audio recording snapshot taken synchronously at log time (was racing the form-clear cancel), Flex COMP meter max -20->-25 and MIC scale to 0; changelog 0.20.10

This commit is contained in:
2026-07-23 01:08:47 +02:00
parent 13de53772b
commit 1070637c40
7 changed files with 142 additions and 62 deletions
+17 -10
View File
@@ -1983,6 +1983,12 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
q.ID = id
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)
// Snapshot the QSO recording SYNCHRONOUSLY, BEFORE announcing the log: the
// qso:logged event clears the entry form, and clearing the callsign cancels
// the recorder — so if this ran on the async path below it raced the cancel
// and the audio was discarded (recordings silently stopped working). The
// snapshot is an in-memory copy; the heavy file encode still runs async.
a.saveQSORecording(&q)
// 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)
@@ -1994,7 +2000,6 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
qc := q
go func() {
a.materializeAwardRefs(qc) // fills the award_refs column
a.saveQSORecording(&qc)
if a.extsvc != nil {
a.extsvc.OnQSOLogged(id)
}
@@ -6226,19 +6231,14 @@ func (a *App) saveQSORecording(q *qso.QSO) {
return
}
// Stamp the recording's path on the QSO now, synchronously, so it's set
// before the eQSL auto-send reads the QSO (their full-row Updates would
// otherwise race and clobber each other's extras).
// Stamp the recording's path on the in-memory QSO now (the encode goroutine
// reads it). The DB write is deferred into that goroutine so this stays OFF
// the critical logging path — saveQSORecording is now called synchronously.
if q.ID != 0 {
if q.Extras == nil {
q.Extras = map[string]string{}
}
q.Extras["APP_OPSLOG_RECORDING"] = name // in-memory copy for the encode goroutine
// Persist ONLY this extras key (targeted) — a full-row Update from this
// in-memory copy could revert a column a concurrent post-log action changed.
if err := a.qso.SetExtra(a.ctx, q.ID, "APP_OPSLOG_RECORDING", name); err != nil {
applog.Printf("qso-rec: store recording path: %v", err)
}
q.Extras["APP_OPSLOG_RECORDING"] = name
}
// Clone the QSO for the goroutine with its OWN copy of the Extras map: a
@@ -6264,6 +6264,13 @@ func (a *App) saveQSORecording(q *qso.QSO) {
applog.Printf("qso-rec: save failed: %v", err)
return
}
// Persist the recording path (targeted extras write) now that the file
// exists — off the critical path, so a busy MySQL never delays logging.
if qc.ID != 0 {
if err := a.qso.SetExtra(a.ctx, qc.ID, "APP_OPSLOG_RECORDING", name); err != nil {
applog.Printf("qso-rec: store recording path: %v", err)
}
}
applog.Printf("qso-rec: saved %s", path)
// Auto-send the recording once the file exists. Gated ONLY on its own
// "auto-send recording" toggle (email.auto_send) — NOT the SMTP panel's