feat: Station Control amp parity + all amps, Help→Send log to F4BPO, bulk-edit frequency, fix Cluster midnight sort
- Station Control: the amplifier panel is now the exact same card as the FlexRadio panel (extracted shared AmpCard: OPERATE/STANDBY, ON/OFF, power level, output-power bar, live FWD/ID/temp meters). Every configured amp gets its own card — no dropdown. - Help → "Send log to F4BPO" (only when SMTP is configured): e-mails opslog.log + previous session + crash log to the developer. New email.SendFiles for multi-attachment. - Bulk edit: new "Frequency (MHz)" field sets freq_hz AND recomputes band; out-of-band values rejected. Fixes a run logged on a stale/default freq after CAT dropped. - Cluster: Time column sorted lexically on the HHMMZ string, so spots after 0000Z sorted below 2359Z and looked frozen at the UTC rollover. Now sorts by real arrival time. - Also folds in the DVK auto-CQ/2-column layout and Station Control dashboard batch (changelog 0.20.10, EN+FR).
This commit is contained in:
@@ -5171,6 +5171,11 @@ func (a *App) BulkUpdateField(ids []int64, field, value string) (int64, error) {
|
||||
if a.qso == nil {
|
||||
return 0, fmt.Errorf("db not initialized")
|
||||
}
|
||||
// Frequency is numeric and drives the band, so it takes a dedicated path
|
||||
// (freq_hz + band updated together) rather than the generic text setter.
|
||||
if field == "freq" {
|
||||
return a.bulkSetFrequency(ids, value)
|
||||
}
|
||||
col, ok := bulkFieldColumns[field]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unknown field %q", field)
|
||||
@@ -5189,6 +5194,34 @@ func (a *App) BulkUpdateField(ids []int64, field, value string) (int64, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// bulkSetFrequency parses a MHz frequency and sets freq_hz + band across the
|
||||
// selected QSOs. Accepts a comma or dot decimal separator (fr locale) and
|
||||
// rejects anything outside the ham bands so a typo can't corrupt the batch.
|
||||
func (a *App) bulkSetFrequency(ids []int64, value string) (int64, error) {
|
||||
s := strings.ReplaceAll(strings.TrimSpace(value), ",", ".")
|
||||
if s == "" {
|
||||
return 0, fmt.Errorf("enter a frequency in MHz (e.g. 7.155)")
|
||||
}
|
||||
mhz, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil || mhz <= 0 {
|
||||
return 0, fmt.Errorf("invalid frequency %q — enter MHz, e.g. 7.155", value)
|
||||
}
|
||||
hz := int64(math.Round(mhz * 1_000_000))
|
||||
band := cat.BandFromHz(hz)
|
||||
if band == "" {
|
||||
return 0, fmt.Errorf("%.4f MHz is not in a ham band", mhz)
|
||||
}
|
||||
n, err := a.qso.BulkSetFrequency(a.ctx, ids, hz, band)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n > 0 {
|
||||
a.invalidateAwardStats()
|
||||
a.materializeAwardRefsForIDs(ids) // band change may shift award slots → refresh
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// WorkedBefore returns prior contacts with the given callsign at both
|
||||
// call and DXCC granularity. Pass dxccHint=0 when unknown — the function
|
||||
// will infer it from past QSOs with the same call when possible.
|
||||
@@ -7178,6 +7211,55 @@ func (a *App) SendQSORecordingEmail(id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// developerEmail is where "Help → Send log to F4BPO" delivers diagnostic logs.
|
||||
const developerEmail = "[email protected]"
|
||||
|
||||
// SMTPConfigured reports whether an SMTP server is set up, so the UI can show the
|
||||
// "Send log to F4BPO" help entry only when there's actually a way to send.
|
||||
func (a *App) SMTPConfigured() bool {
|
||||
s, _ := a.GetEmailSettings()
|
||||
return strings.TrimSpace(s.Host) != ""
|
||||
}
|
||||
|
||||
// SendLogToDeveloper e-mails the app's diagnostic logs (opslog.log, the previous
|
||||
// session's opslog.log.1, and any crash log) to the developer so problems users
|
||||
// hit in the field can be diagnosed. Requires a configured SMTP server
|
||||
// (Settings → E-mail). A short header identifies the sender's call and version.
|
||||
func (a *App) SendLogToDeveloper() error {
|
||||
s, _ := a.GetEmailSettings()
|
||||
if strings.TrimSpace(s.Host) == "" {
|
||||
return fmt.Errorf("no SMTP server configured — set one in Settings → E-mail")
|
||||
}
|
||||
logPath := applog.Path()
|
||||
if logPath == "" {
|
||||
return fmt.Errorf("log file not available")
|
||||
}
|
||||
// The current log, plus the rotated one and the crash log when they exist.
|
||||
attach := []string{logPath}
|
||||
for _, extra := range []string{logPath + ".1", filepath.Join(filepath.Dir(logPath), "opslog-crash.log")} {
|
||||
if _, err := os.Stat(extra); err == nil {
|
||||
attach = append(attach, extra)
|
||||
}
|
||||
}
|
||||
call := ""
|
||||
if a.profiles != nil {
|
||||
if p, err := a.profiles.Active(a.ctx); err == nil {
|
||||
call = p.Callsign
|
||||
}
|
||||
}
|
||||
subject := fmt.Sprintf("OpsLog log — v%s", appVersion)
|
||||
if call != "" {
|
||||
subject = fmt.Sprintf("OpsLog log — %s v%s", call, appVersion)
|
||||
}
|
||||
body := fmt.Sprintf("OpsLog diagnostic log attached.\n\nCallsign: %s\nVersion: %s\n\n73", call, appVersion)
|
||||
if err := email.SendFiles(a.emailConfig(s), developerEmail, subject, body, attach); err != nil {
|
||||
applog.Printf("email: send log to developer failed: %v", err)
|
||||
return err
|
||||
}
|
||||
applog.Printf("email: diagnostic log sent to developer (%s)", developerEmail)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── ClubLog Country File (cty.xml) exceptions ─────────────────────────
|
||||
|
||||
// ClublogCtyInfo is the UI status of the ClubLog exception data.
|
||||
|
||||
Reference in New Issue
Block a user