chore: release v0.19.3

This commit is contained in:
2026-07-09 17:32:13 +02:00
parent 19c2de5f61
commit 1f74e4d234
11 changed files with 482 additions and 9 deletions
+68
View File
@@ -47,6 +47,7 @@ import (
"hamlog/internal/qso"
"hamlog/internal/rotator/pst"
"hamlog/internal/settings"
"hamlog/internal/solar"
"hamlog/internal/ultrabeam"
"hamlog/internal/winkeyer"
@@ -420,6 +421,7 @@ type App struct {
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
solar *solar.Manager // live space-weather (SFI/SSN/A/K) for the header + QSO stamping
// NET Control: persistent net definitions/rosters (global JSON) + the live
// session (in-memory only — active stations currently in QSO).
@@ -874,6 +876,16 @@ func (a *App) startup(ctx context.Context) {
},
)
// Live space-weather (solar flux, sunspots, A/K indices) for the header strip
// and per-QSO stamping. Refreshes in the background; pushes a UI event on each
// update so the strip re-reads.
a.solar = solar.NewManager(func() {
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "solar:update")
}
})
a.solar.Start()
// Digital Voice Keyer + QSO recorder (WASAPI). Idle until used.
a.audioMgr = audio.NewManager(func() {
st := a.dvkStatus()
@@ -1116,6 +1128,9 @@ func (a *App) shutdown(ctx context.Context) {
if a.udp != nil {
a.udp.StopAll()
}
if a.solar != nil {
a.solar.Stop()
}
// Stop CAT so the backend disconnects cleanly. Critical for the Icom network
// backend: without this the rig never gets a disconnect and holds its single
// control session for minutes, refusing every new login (even from the Icom
@@ -1643,6 +1658,7 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
a.applyClublogException(&q, false) // override entity for date-ranged DXpeditions
a.refineDistrictZones(&q) // W6 → CQ3/ITU6 for zone-split countries
a.applyQSLDefaults(&q)
a.applySolar(&q) // stamp SFI / A / K (and SSN as an extra) from live space-weather
// Fill the contacted operator's e-mail from the (cached) lookup so the
// recording can be auto-sent. Cheap: the entry already looked the call up.
if strings.TrimSpace(q.Email) == "" && a.lookup != nil {
@@ -1666,6 +1682,58 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
return id, err
}
// applySolar stamps the current space-weather onto a QSO before it's saved: the
// standard ADIF SFI / A_INDEX / K_INDEX fields (only when the QSO doesn't already
// carry them), and the sunspot number as an APP_OPSLOG_SSN extra (SSN has no
// standard ADIF field). No-op when the feed hasn't loaded yet.
func (a *App) applySolar(q *qso.QSO) {
if a.solar == nil {
return
}
d := a.solar.Get()
if !d.OK {
return
}
if q.SFI == nil {
if v, err := strconv.ParseFloat(strings.TrimSpace(d.SFI), 64); err == nil {
q.SFI = &v
}
}
if q.AIndex == nil {
if v, err := strconv.ParseFloat(strings.TrimSpace(d.AIndex), 64); err == nil {
q.AIndex = &v
}
}
if q.KIndex == nil {
if v, err := strconv.ParseFloat(strings.TrimSpace(d.KIndex), 64); err == nil {
q.KIndex = &v
}
}
if ssn := strings.TrimSpace(d.SSN); ssn != "" {
if q.Extras == nil {
q.Extras = map[string]string{}
}
if _, ok := q.Extras["APP_OPSLOG_SSN"]; !ok {
q.Extras["APP_OPSLOG_SSN"] = ssn
}
}
}
// GetSolarData returns the latest space-weather snapshot for the header strip.
func (a *App) GetSolarData() solar.Data {
if a.solar == nil {
return solar.Data{}
}
return a.solar.Get()
}
// RefreshSolar forces an immediate re-fetch of the space-weather feed.
func (a *App) RefreshSolar() {
if a.solar != nil {
go a.solar.Refresh()
}
}
// StationInfoComputed bundles the data we resolve live from the
// profile's callsign + grid: country, ARRL DXCC#, CQ zone, ITU zone,
// lat/lon. Used by the Settings UI to show the "what will be stamped on