This commit is contained in:
2026-05-28 21:32:46 +02:00
parent e8cac569e3
commit e82e30dd02
29 changed files with 2485 additions and 97 deletions
+448 -16
View File
@@ -14,10 +14,12 @@ import (
"time"
"hamlog/internal/adif"
"hamlog/internal/applog"
"hamlog/internal/backup"
"hamlog/internal/cat"
"hamlog/internal/cluster"
"hamlog/internal/db"
"hamlog/internal/integrations/udp"
"hamlog/internal/operating"
"hamlog/internal/dxcc"
"hamlog/internal/lookup"
@@ -72,8 +74,33 @@ const (
keyBackupRotation = "backup.rotation"
keyBackupZip = "backup.zip"
keyBackupLast = "backup.last_at"
keyQSLDefaultQSLSent = "qsl.qsl_sent"
keyQSLDefaultQSLRcvd = "qsl.qsl_rcvd"
keyQSLDefaultLOTWSent = "qsl.lotw_sent"
keyQSLDefaultLOTWRcvd = "qsl.lotw_rcvd"
keyQSLDefaultEQSLSent = "qsl.eqsl_sent"
keyQSLDefaultEQSLRcvd = "qsl.eqsl_rcvd"
keyQSLDefaultClublogStatus = "qsl.clublog_status"
keyQSLDefaultHRDLogStatus = "qsl.hrdlog_status"
)
// QSLDefaults is the per-user default for the QSL / eQSL / LoTW / upload
// status fields. Applied to every QSO when the corresponding field is
// empty — both manual entry and UDP auto-log. Values are ADIF status
// codes: "Y" yes, "N" no, "R" requested, "Q" queued, "I" ignore, ""
// (empty) leaves the field untouched.
type QSLDefaults struct {
QSLSent string `json:"qsl_sent"`
QSLRcvd string `json:"qsl_rcvd"`
LOTWSent string `json:"lotw_sent"`
LOTWRcvd string `json:"lotw_rcvd"`
EQSLSent string `json:"eqsl_sent"`
EQSLRcvd string `json:"eqsl_rcvd"`
ClublogStatus string `json:"clublog_status"`
HRDLogStatus string `json:"hrdlog_status"`
}
// CATSettings is the user-tweakable rig-control configuration. Stored as
// individual key/value pairs to keep the settings table flat.
type CATSettings struct {
@@ -155,6 +182,8 @@ type App struct {
dxcc *dxcc.Manager
cluster *cluster.Manager
operating *operating.Repo
udp *udp.Manager
udpRepo *udp.Repo
startupErr string // captured for surfacing to the frontend
dbPath string
@@ -277,19 +306,30 @@ func (a *App) startup(ctx context.Context) {
dataDir, err := userDataDir()
if err != nil {
a.startupErr = "cannot resolve data dir: " + err.Error()
fmt.Println("HamLog:", a.startupErr)
fmt.Println("OpsLog:", a.startupErr)
return
}
if err := os.MkdirAll(dataDir, 0o755); err != nil {
a.startupErr = "cannot create data dir: " + err.Error()
fmt.Println("HamLog:", a.startupErr)
fmt.Println("OpsLog:", a.startupErr)
return
}
a.dbPath = filepath.Join(dataDir, "hamlog.db")
a.dbPath = filepath.Join(dataDir, "opslog.db")
// One-shot rename for users coming from the HamLog era.
if _, err := os.Stat(a.dbPath); os.IsNotExist(err) {
oldDB := filepath.Join(dataDir, "hamlog.db")
if _, err := os.Stat(oldDB); err == nil {
_ = os.Rename(oldDB, a.dbPath)
}
}
if _, err := applog.Init(dataDir); err != nil {
fmt.Println("OpsLog: log init:", err)
}
applog.Printf("startup: data dir = %s", dataDir)
conn, err := db.Open(a.dbPath)
if err != nil {
a.startupErr = "cannot open db: " + err.Error()
fmt.Println("HamLog:", a.startupErr)
fmt.Println("OpsLog:", a.startupErr)
return
}
a.db = conn
@@ -297,6 +337,9 @@ func (a *App) startup(ctx context.Context) {
a.settings = settings.NewStore(conn)
a.profiles = profile.NewRepo(conn)
a.operating = operating.NewRepo(conn)
a.udpRepo = udp.NewRepo(conn)
a.udp = udp.NewManager(a.udpRepo)
go a.consumeUDPEvents()
// On first run, copy the legacy single-station settings into a
// "Default" profile so the user's existing config carries over without
// any manual step. Subsequent runs just confirm an active profile.
@@ -308,7 +351,7 @@ func (a *App) startup(ctx context.Context) {
SOTA: keyStationSOTA,
POTA: keyStationPOTA,
}); err != nil {
fmt.Println("HamLog: EnsureDefault profile:", err)
fmt.Println("OpsLog: EnsureDefault profile:", err)
}
a.cache = lookup.NewCache(conn, 30*24*time.Hour)
a.lookup = lookup.NewManager(a.cache)
@@ -321,10 +364,10 @@ func (a *App) startup(ctx context.Context) {
a.lookup.SetDXCCResolver(dxccAdapter{m: a.dxcc})
go func() {
if err := a.dxcc.EnsureLoaded(context.Background()); err != nil {
fmt.Println("HamLog: cty.dat unavailable —", err)
fmt.Println("OpsLog: cty.dat unavailable —", err)
return
}
fmt.Println("HamLog: cty.dat loaded —", a.dxcc.Info().Entities, "entities")
fmt.Println("OpsLog: cty.dat loaded —", a.dxcc.Info().Entities, "entities")
}()
// CAT manager: emit pushes state to the frontend via Wails events.
a.cat = cat.NewManager(func(s cat.RigState) {
@@ -368,8 +411,13 @@ func (a *App) startup(ctx context.Context) {
if cs, _ := a.clusterAutoConnect(); cs {
a.startAllEnabledClusters()
}
if errs := a.udp.Reload(a.ctx); len(errs) > 0 {
for _, e := range errs {
fmt.Println("OpsLog: udp:", e)
}
}
fmt.Println("HamLog: db ready at", a.dbPath)
fmt.Println("OpsLog: db ready at", a.dbPath)
}
// StartupStatus returns a diagnostic snapshot for the frontend.
@@ -502,17 +550,33 @@ func (a *App) shutdown(ctx context.Context) {
if !a.shuttingDown {
a.maybeShutdownBackup()
}
if a.udp != nil {
a.udp.StopAll()
}
if a.db != nil {
_ = a.db.Close()
}
}
// userDataDir returns the OpsLog data directory under the user's config
// dir. The app was previously called HamLog — if the old folder exists
// and the new one doesn't, we rename it atomically so the user keeps
// their database, settings and cluster history through the rebrand.
func userDataDir() (string, error) {
base, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(base, "HamLog"), nil
newDir := filepath.Join(base, "OpsLog")
oldDir := filepath.Join(base, "HamLog")
if _, err := os.Stat(newDir); os.IsNotExist(err) {
if _, err := os.Stat(oldDir); err == nil {
// One-shot migration: HamLog → OpsLog. Best-effort: on
// failure we fall through and create OpsLog fresh.
_ = os.Rename(oldDir, newDir)
}
}
return newDir, nil
}
// reloadLookupProviders rebuilds the lookup chain from current settings.
@@ -529,7 +593,7 @@ func (a *App) reloadLookupProviders() {
keyQRZUser, keyQRZPassword, keyHQUser, keyHQPassword,
keyCacheTTL, keyLookupPrimary, keyLookupFailsafe)
if err != nil {
fmt.Println("HamLog: settings load error:", err)
fmt.Println("OpsLog: settings load error:", err)
return
}
if days, _ := strconv.Atoi(m[keyCacheTTL]); days > 0 {
@@ -582,9 +646,60 @@ func (a *App) AddQSO(q qso.QSO) (int64, error) {
return 0, fmt.Errorf("db not initialized")
}
a.applyStationDefaults(&q)
a.applyDXCCNumber(&q)
a.applyQSLDefaults(&q)
return a.qso.Add(a.ctx, q)
}
// 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
// each QSO" preview next to the editable fields.
type StationInfoComputed struct {
Country string `json:"country"`
DXCC int `json:"dxcc"`
CQZ int `json:"cqz"`
ITUZ int `json:"ituz"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
// ComputeStationInfo resolves a station's structured metadata from the
// callsign (via cty.dat) and grid (via Maidenhead → lat/lon). The
// frontend calls this whenever Callsign or Grid changes in the Station
// Information panel so the user sees the auto-filled values live.
func (a *App) ComputeStationInfo(callsign, grid string) StationInfoComputed {
var out StationInfoComputed
if a.dxcc != nil && callsign != "" {
if m, ok := a.dxcc.Lookup(callsign); ok && m.Entity != nil {
out.Country = m.Entity.Name
out.CQZ = m.CQZone
out.ITUZ = m.ITUZone
out.Lat = m.Lat
out.Lon = m.Lon
out.DXCC = dxcc.EntityDXCC(m.Entity.Name)
}
}
// Grid wins on lat/lon — it's user-set, finer than the DXCC centroid.
if lat, lon, ok := gridToLatLon(grid); ok {
out.Lat = lat
out.Lon = lon
}
return out
}
// applyDXCCNumber fills DXCC (contacted station) from the cty.dat entity
// name when it's empty. Same lookup as applyStationDefaults does for
// MY_DXCC — uses our entity-name → ADIF DXCC# table since cty.dat itself
// doesn't store the ARRL number.
func (a *App) applyDXCCNumber(q *qso.QSO) {
if q.DXCC == nil && q.Country != "" {
if n := dxcc.EntityDXCC(q.Country); n != 0 {
q.DXCC = &n
}
}
}
// applyStationDefaults fills any empty MY_* / station field on q with the
// currently-active profile's values. Multi-profile support means a user
// can be /P with a different callsign + grid + SOTA ref than home — the
@@ -640,6 +755,53 @@ func (a *App) applyStationDefaults(q *qso.QSO) {
v := *p.TxPower
q.TXPower = &v
}
// Resolve my zones / lat / lon via cty.dat using the profile's
// callsign. The profile only stores the human-friendly fields
// (callsign, grid, country name); cty.dat fills the structured
// DXCC metadata that the ADIF spec wants for every QSO.
if a.dxcc != nil && p.Callsign != "" {
if m, ok := a.dxcc.Lookup(p.Callsign); ok && m.Entity != nil {
if q.MyCQZone == nil && m.CQZone != 0 {
v := m.CQZone
q.MyCQZone = &v
}
if q.MyITUZone == nil && m.ITUZone != 0 {
v := m.ITUZone
q.MyITUZone = &v
}
if q.MyCountry == "" && m.Entity.Name != "" {
q.MyCountry = m.Entity.Name
}
if q.MyDXCC == nil {
if n := dxcc.EntityDXCC(m.Entity.Name); n != 0 {
q.MyDXCC = &n
}
}
// Lat/Lon: prefer the profile's grid (more precise than the
// DXCC entity centroid). Fall back to cty.dat coordinates.
if q.MyLat == nil || q.MyLon == nil {
if lat, lon, gOK := gridToLatLon(p.MyGrid); gOK {
if q.MyLat == nil {
v := lat
q.MyLat = &v
}
if q.MyLon == nil {
v := lon
q.MyLon = &v
}
} else {
if q.MyLat == nil && m.Lat != 0 {
v := m.Lat
q.MyLat = &v
}
if q.MyLon == nil && m.Lon != 0 {
v := m.Lon
q.MyLon = &v
}
}
}
}
}
}
func (a *App) ListQSO(f qso.ListFilter) ([]qso.QSO, error) {
@@ -750,9 +912,9 @@ func (a *App) ImportADIF(path string, skipDuplicates bool) (adif.ImportResult, e
}
// SaveADIFFile shows a native Save-As dialog suggesting a timestamped
// HamLog_YYYYMMDD_HHMMSS.adi filename. Returns "" if the user cancelled.
// OpsLog_YYYYMMDD_HHMMSS.adi filename. Returns "" if the user cancelled.
func (a *App) SaveADIFFile() (string, error) {
suggested := "HamLog_" + time.Now().UTC().Format("20060102_150405") + ".adi"
suggested := "OpsLog_" + time.Now().UTC().Format("20060102_150405") + ".adi"
return wruntime.SaveFileDialog(a.ctx, wruntime.SaveDialogOptions{
Title: "Export ADIF",
DefaultFilename: suggested,
@@ -772,7 +934,7 @@ func (a *App) ExportADIF(path string) (adif.ExportResult, error) {
if path == "" {
return adif.ExportResult{}, fmt.Errorf("empty path")
}
ex := &adif.Exporter{Repo: a.qso, AppName: "HamLog", AppVersion: "0.1"}
ex := &adif.Exporter{Repo: a.qso, AppName: "OpsLog", AppVersion: "0.1"}
return ex.ExportFile(a.ctx, path)
}
@@ -989,6 +1151,276 @@ func (a *App) SaveCATSettings(s CATSettings) error {
return nil
}
// GetLogFilePath returns where the diagnostic log file lives so the user
// can open it from the Settings UI. Empty when applog hasn't initialised.
func (a *App) GetLogFilePath() string {
return applog.Path()
}
// ── QSL defaults ──────────────────────────────────────────────────────
// GetQSLDefaults returns the stored defaults — empty strings when the
// user hasn't configured anything (= leave QSO fields untouched).
func (a *App) GetQSLDefaults() (QSLDefaults, error) {
out := QSLDefaults{}
if a.settings == nil {
return out, nil
}
m, err := a.settings.GetMany(a.ctx,
keyQSLDefaultQSLSent, keyQSLDefaultQSLRcvd,
keyQSLDefaultLOTWSent, keyQSLDefaultLOTWRcvd,
keyQSLDefaultEQSLSent, keyQSLDefaultEQSLRcvd,
keyQSLDefaultClublogStatus, keyQSLDefaultHRDLogStatus,
)
if err != nil {
return out, err
}
out.QSLSent = m[keyQSLDefaultQSLSent]
out.QSLRcvd = m[keyQSLDefaultQSLRcvd]
out.LOTWSent = m[keyQSLDefaultLOTWSent]
out.LOTWRcvd = m[keyQSLDefaultLOTWRcvd]
out.EQSLSent = m[keyQSLDefaultEQSLSent]
out.EQSLRcvd = m[keyQSLDefaultEQSLRcvd]
out.ClublogStatus = m[keyQSLDefaultClublogStatus]
out.HRDLogStatus = m[keyQSLDefaultHRDLogStatus]
return out, nil
}
// SaveQSLDefaults persists the configured defaults. Future QSO inserts
// pick them up automatically — no app restart needed.
func (a *App) SaveQSLDefaults(d QSLDefaults) error {
if a.settings == nil {
return fmt.Errorf("db not initialized")
}
for k, v := range map[string]string{
keyQSLDefaultQSLSent: strings.ToUpper(strings.TrimSpace(d.QSLSent)),
keyQSLDefaultQSLRcvd: strings.ToUpper(strings.TrimSpace(d.QSLRcvd)),
keyQSLDefaultLOTWSent: strings.ToUpper(strings.TrimSpace(d.LOTWSent)),
keyQSLDefaultLOTWRcvd: strings.ToUpper(strings.TrimSpace(d.LOTWRcvd)),
keyQSLDefaultEQSLSent: strings.ToUpper(strings.TrimSpace(d.EQSLSent)),
keyQSLDefaultEQSLRcvd: strings.ToUpper(strings.TrimSpace(d.EQSLRcvd)),
keyQSLDefaultClublogStatus: strings.ToUpper(strings.TrimSpace(d.ClublogStatus)),
keyQSLDefaultHRDLogStatus: strings.ToUpper(strings.TrimSpace(d.HRDLogStatus)),
} {
if err := a.settings.Set(a.ctx, k, v); err != nil {
return err
}
}
return nil
}
// applyQSLDefaults stamps the user-configured defaults onto a QSO when
// the corresponding fields are still empty. Called from every save path
// (manual entry via AddQSO, UDP auto-log via LogUDPLoggedADIF) so the
// confirmations columns always reflect the user's preferences.
func (a *App) applyQSLDefaults(q *qso.QSO) {
if a.settings == nil {
return
}
d, err := a.GetQSLDefaults()
if err != nil {
return
}
if q.QSLSent == "" { q.QSLSent = d.QSLSent }
if q.QSLRcvd == "" { q.QSLRcvd = d.QSLRcvd }
if q.LOTWSent == "" { q.LOTWSent = d.LOTWSent }
if q.LOTWRcvd == "" { q.LOTWRcvd = d.LOTWRcvd }
if q.EQSLSent == "" { q.EQSLSent = d.EQSLSent }
if q.EQSLRcvd == "" { q.EQSLRcvd = d.EQSLRcvd }
if q.ClublogUploadStatus == "" { q.ClublogUploadStatus = d.ClublogStatus }
if q.HRDLogUploadStatus == "" { q.HRDLogUploadStatus = d.HRDLogStatus }
}
// ── UDP integrations ───────────────────────────────────────────────────
// ListUDPIntegrations returns every saved UDP connection row.
func (a *App) ListUDPIntegrations() ([]udp.Config, error) {
if a.udpRepo == nil {
return nil, fmt.Errorf("db not initialized")
}
return a.udpRepo.List(a.ctx)
}
// SaveUDPIntegration upserts a UDP connection and reloads the manager so
// inbound listeners pick up the change without an app restart. Reload
// errors are surfaced — a "port already in use" failure should reach the
// user rather than be silently dropped.
func (a *App) SaveUDPIntegration(c udp.Config) (udp.Config, error) {
if a.udpRepo == nil {
return c, fmt.Errorf("db not initialized")
}
if err := a.udpRepo.Save(a.ctx, &c); err != nil {
return c, err
}
if a.udp != nil {
errs := a.udp.Reload(a.ctx)
if len(errs) > 0 {
return c, fmt.Errorf("listener errors: %s", strings.Join(errs, "; "))
}
}
return c, nil
}
// DeleteUDPIntegration removes a row and reloads the manager.
func (a *App) DeleteUDPIntegration(id int64) error {
if a.udpRepo == nil {
return fmt.Errorf("db not initialized")
}
if err := a.udpRepo.Delete(a.ctx, id); err != nil {
return err
}
if a.udp != nil {
a.udp.Reload(a.ctx)
}
return nil
}
// ReloadUDPIntegrations is a no-arg way for the UI to force a restart
// (e.g. after toggling Enabled on a row).
func (a *App) ReloadUDPIntegrations() []string {
if a.udp == nil {
return nil
}
return a.udp.Reload(a.ctx)
}
// LogUDPLoggedADIF takes an ADIF blob received over UDP and inserts the
// first record into the local logbook. Returns the ID of the inserted
// row. Used by the auto-log handler (WSJT-X / JTDX / MSHV / JTAlert).
func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
if a.qso == nil {
return 0, fmt.Errorf("db not initialized")
}
// Pull the first record out of the payload. WSJT-X / JTDX / MSHV
// always send a single QSO per UDP packet (no header) but we tolerate
// either form via adif.Parse.
var record adif.Record
err := adif.Parse(strings.NewReader(adifText), func(rec adif.Record) error {
if record == nil {
record = rec
}
return nil
})
if err != nil {
return 0, fmt.Errorf("parse adif: %w", err)
}
if record == nil {
// Some senders skip the <EOH> header; try treating the whole
// payload as a single record by prepending a fake header.
err := adif.Parse(strings.NewReader("<EOH>"+adifText), func(rec adif.Record) error {
if record == nil {
record = rec
}
return nil
})
if err != nil || record == nil {
return 0, fmt.Errorf("no valid QSO record in payload")
}
}
q, ok := adif.RecordToQSO(record)
if !ok {
return 0, fmt.Errorf("record missing required fields (call/band/mode/date)")
}
// ── Lookup-based enrichment ──
// WSJT sends only call/freq/mode/RST/date. Fill Name/QTH/Country/
// Grid/CQZ/ITUZ/DXCC/Continent via the lookup chain (QRZ/HamQTH/
// cty.dat). Best-effort: a network failure shouldn't block the log.
if a.lookup != nil {
if lr, lerr := a.lookup.Lookup(a.ctx, q.Callsign); lerr == nil {
if q.Name == "" { q.Name = lr.Name }
if q.QTH == "" { q.QTH = lr.QTH }
if q.Country == "" { q.Country = lr.Country }
if q.Grid == "" { q.Grid = lr.Grid }
if q.Continent == "" { q.Continent = lr.Continent }
if q.State == "" { q.State = lr.State }
if q.County == "" { q.County = lr.County }
if q.Address == "" { q.Address = lr.Address }
if q.Email == "" { q.Email = lr.Email }
if q.DXCC == nil && lr.DXCC != 0 { v := lr.DXCC; q.DXCC = &v }
if q.CQZ == nil && lr.CQZ != 0 { v := lr.CQZ; q.CQZ = &v }
if q.ITUZ == nil && lr.ITUZ != 0 { v := lr.ITUZ; q.ITUZ = &v }
if q.Lat == nil && lr.Lat != 0 { v := lr.Lat; q.Lat = &v }
if q.Lon == nil && lr.Lon != 0 { v := lr.Lon; q.Lon = &v }
}
}
// ── Operating-conditions stamp ──
// Pre-fill MY_RIG / MY_ANTENNA / TX_PWR from the default antenna for
// this band (if the user has configured Operating conditions).
if a.operating != nil && a.profiles != nil {
if p, err := a.profiles.Active(a.ctx); err == nil {
if d, ok2, _ := a.operating.BandDefault(a.ctx, p.ID, q.Band); ok2 {
if q.MyRig == "" { q.MyRig = d.StationName }
if q.MyAntenna == "" { q.MyAntenna = d.AntennaName }
if q.TXPower == nil && d.TXPower != nil { v := *d.TXPower; q.TXPower = &v }
}
}
}
// ── DXCC# + QSL defaults ──
// applyDXCCNumber stamps the contacted-station DXCC# from the
// entity-name table; QSL defaults are applied last so explicit ADIF
// fields (or what the lookup gave us) always win.
a.applyDXCCNumber(&q)
a.applyQSLDefaults(&q)
// ── Dedup ──
// Match by call + minute + band + mode (same key the importer uses).
seen, err := a.qso.ExistingDedupeKeys(a.ctx)
if err == nil {
key := qso.DedupeKey(q.Callsign, q.QSODate.UTC().Format("2006-01-02T15:04"), q.Band, q.Mode)
if _, dup := seen[key]; dup {
return 0, fmt.Errorf("duplicate (already in log)")
}
}
id, err := a.qso.Add(a.ctx, q)
if err != nil {
return 0, fmt.Errorf("insert qso: %w", err)
}
return id, nil
}
// consumeUDPEvents bridges parsed UDP events to the frontend over Wails'
// event bus. The frontend listens on:
// udp:dx_call → string callsign (also Grid/Mode/Freq when known)
// udp:logged_qso → ADIF text of a QSO that finished in WSJT-X/JTDX/MSHV
// udp:remote_call → string callsign from a remote-control source
func (a *App) consumeUDPEvents() {
if a.udp == nil {
return
}
for ev := range a.udp.Events() {
if a.ctx == nil {
continue
}
switch {
case ev.LoggedADIF != "":
applog.Printf("udp: emit udp:logged_qso (%d bytes ADIF)\n", len(ev.LoggedADIF))
wruntime.EventsEmit(a.ctx, "udp:logged_qso", map[string]any{
"config_id": ev.ConfigID,
"service": string(ev.Service),
"source": ev.Source,
"adif": ev.LoggedADIF,
})
case ev.DXCall != "" && ev.Service == udp.ServiceRemoteCall:
applog.Printf("udp: emit udp:remote_call %q\n", ev.DXCall)
wruntime.EventsEmit(a.ctx, "udp:remote_call", ev.DXCall)
case ev.DXCall != "":
applog.Printf("udp: emit udp:dx_call %q (mode=%s freq=%d)\n", ev.DXCall, ev.Mode, ev.FreqHz)
wruntime.EventsEmit(a.ctx, "udp:dx_call", map[string]any{
"call": ev.DXCall,
"grid": ev.DXGrid,
"mode": ev.Mode,
"freq_hz": ev.FreqHz,
"service": string(ev.Service),
"source": ev.Source,
})
}
}
}
// ── Operating conditions ───────────────────────────────────────────────
// ListOperatingTree returns the stations/antennas/bands tree for the
@@ -1173,7 +1605,7 @@ func (a *App) maybeShutdownBackup() {
return
}
if _, err := backup.Run(a.ctx, a.db, a.dbPath, folder, s.Rotation, s.Zip); err != nil {
fmt.Println("HamLog: shutdown backup failed:", err)
fmt.Println("OpsLog: shutdown backup failed:", err)
return
}
_ = a.settings.Set(a.ctx, keyBackupLast, time.Now().UTC().Format(time.RFC3339))
@@ -1198,7 +1630,7 @@ func (a *App) PickBackupFolder() (string, error) {
}
defaultDir = firstExistingAncestor(defaultDir)
return wruntime.OpenDirectoryDialog(a.ctx, wruntime.OpenDialogOptions{
Title: "Pick a folder for HamLog backups",
Title: "Pick a folder for OpsLog backups",
DefaultDirectory: defaultDir,
})
}
@@ -1645,7 +2077,7 @@ func (a *App) clusterAutoConnect() (bool, error) {
func (a *App) startAllEnabledClusters() {
servers, err := a.listClusterServers()
if err != nil {
fmt.Println("HamLog: list cluster servers:", err)
fmt.Println("OpsLog: list cluster servers:", err)
return
}
for _, s := range servers {