feat(hamlog): the settings panel, the key check, and a row-colour channel

HAMLOG.online is now reachable: Settings → External services → HAMLOG.ONLINE
takes the API key, the auto-upload switch and the timing, exactly like the six
services beside it, and a link opens the page where the key is issued.

The button next to it checks the KEY, not the connection. HAMLOG answers
KEYSTATUS with the callsign the key belongs to — something no other service
here offers — so the result reads "Key is valid — account F4BPO" rather than a
green tick. A key pasted from a club account or a second station is visible at
once, which is precisely the mistake a tick would have hidden.

No on-close timing, for the same reason as Cloudlog: there is no per-QSO upload
column to select a backlog from at shutdown, so the option would have done
nothing. Immediate or delayed, and a stored on_close falls back to immediate
rather than silently uploading nothing.

The appearance rules gain a HAMLOG.online channel too, so a row can be coloured
"confirmed" on that alone. It reads the ADIF extras — the standard names a field
for hamlog.EU and none for hamlog.ONLINE — using the same keys the award engine
reads, so the two can never disagree about what a confirmation is. "To send"
skips it: a QSO is either uploaded or not, there is no card to be owed.
This commit is contained in:
2026-08-23 12:29:38 +02:00
parent ee148732dd
commit 73806cbd70
9 changed files with 167 additions and 19 deletions
+38 -1
View File
@@ -382,6 +382,10 @@ const (
keyExtCloudlogAutoUpload = "extsvc.cloudlog.auto_upload"
keyExtCloudlogUploadMode = "extsvc.cloudlog.upload_mode"
keyExtHamlogAPIKey = "extsvc.hamlog.api_key"
keyExtHamlogAutoUpload = "extsvc.hamlog.auto_upload"
keyExtHamlogUploadMode = "extsvc.hamlog.upload_mode"
keyExtPotaToken = "extsvc.pota.token" // pota.app session token for hunter-log sync
keyExtLoTWTQSLPath = "extsvc.lotw.tqsl_path"
@@ -10701,7 +10705,8 @@ func (a *App) loadExternalServices() extsvc.ExternalServices {
keyExtHRDLogCallsign, keyExtHRDLogCode, keyExtHRDLogAutoUpload, keyExtHRDLogUploadMode, keyExtHRDLogOnAir,
keyExtEQSLUsername, keyExtEQSLPassword, keyExtEQSLQTHNick, keyExtEQSLAutoUpload, keyExtEQSLUploadMode,
keyExtCloudlogURL, keyExtCloudlogAPIKey, keyExtCloudlogStationID,
keyExtCloudlogAutoUpload, keyExtCloudlogUploadMode, keyExtDeleteRemote)
keyExtCloudlogAutoUpload, keyExtCloudlogUploadMode, keyExtDeleteRemote,
keyExtHamlogAPIKey, keyExtHamlogAutoUpload, keyExtHamlogUploadMode)
if err != nil {
return out
}
@@ -10780,6 +10785,11 @@ func (a *App) loadExternalServices() extsvc.ExternalServices {
AutoUpload: m[keyExtCloudlogAutoUpload] == "1",
UploadMode: extsvc.UploadMode(m[keyExtCloudlogUploadMode]),
}
out.Hamlog = extsvc.ServiceConfig{
APIKey: m[keyExtHamlogAPIKey],
AutoUpload: m[keyExtHamlogAutoUpload] == "1",
UploadMode: extsvc.UploadMode(m[keyExtHamlogUploadMode]),
}
return out
}
@@ -10848,6 +10858,16 @@ func (a *App) SaveExternalServices(cfg extsvc.ExternalServices) error {
if cfg.Cloudlog.AutoUpload {
clgAuto = "1"
}
// Same reasoning as Cloudlog: HAMLOG has no per-QSO upload-status column to
// select on, so an on-close sweep would have nothing to sweep.
hamMode := modeOf(cfg.Hamlog.UploadMode)
if hamMode == string(extsvc.ModeOnClose) {
hamMode = string(extsvc.ModeImmediate)
}
hamAuto := "0"
if cfg.Hamlog.AutoUpload {
hamAuto = "1"
}
delRemote := "0"
if cfg.DeleteRemote {
delRemote = "1"
@@ -10895,6 +10915,10 @@ func (a *App) SaveExternalServices(cfg extsvc.ExternalServices) error {
keyExtCloudlogStationID: strings.TrimSpace(cfg.Cloudlog.StationID),
keyExtCloudlogAutoUpload: clgAuto,
keyExtCloudlogUploadMode: clgMode,
keyExtHamlogAPIKey: strings.TrimSpace(cfg.Hamlog.APIKey),
keyExtHamlogAutoUpload: hamAuto,
keyExtHamlogUploadMode: hamMode,
} {
if err := a.settings.Set(a.ctx, scope+k, v); err != nil {
return err
@@ -20350,3 +20374,16 @@ func (a *App) countryFor(call string) string {
}
return ""
}
// CheckHamlogKey validates a HAMLOG.online API key and returns the callsign it
// belongs to.
//
// Its own binding rather than part of a save: the answer names the ACCOUNT, so
// an operator who pasted a club's key, or one that expired last month, learns it
// while looking at the field they just filled in — not from QSOs that quietly
// never arrive.
func (a *App) CheckHamlogKey(key string) (string, error) {
ctx, cancel := context.WithTimeout(a.ctx, 20*time.Second)
defer cancel()
return extsvc.CheckHamlogKey(ctx, nil, key)
}