feat(lookup): optional QRZ nickname as the logged name
QRZ publishes <nickname> — the name an operator goes BY on the air — and OpsLog was composing fname + name instead. "Bob" is what belongs in a log; "Robert J Smith" is what belongs on a licence. QRZ only, and deliberately so: HamQTH's <nick> already fills the Name field that way, so the same switch there would toggle a behaviour it has no way to turn off. A published nickname is optional, so an empty one falls through to the registered name. That is the whole point of it being a fallback rather than a swap, and it is what the test pins — a blank nickname must never blank the name.
This commit is contained in:
@@ -78,6 +78,7 @@ import (
|
||||
const (
|
||||
keyQRZUser = "lookup.qrz.user"
|
||||
keyQRZPassword = "lookup.qrz.password"
|
||||
keyQRZNickname = "lookup.qrz.prefer_nickname" // "1" → QRZ <nickname> over first+last name
|
||||
keyHQUser = "lookup.hamqth.user"
|
||||
keyHQPassword = "lookup.hamqth.password"
|
||||
keyCacheTTL = "lookup.cache.ttl_days"
|
||||
@@ -564,6 +565,10 @@ type LookupSettings struct {
|
||||
Primary string `json:"primary"`
|
||||
Failsafe string `json:"failsafe"`
|
||||
DownloadImages bool `json:"download_images"` // show QRZ profile pictures in the UI
|
||||
// QRZPreferNickname logs the name an operator goes BY rather than their
|
||||
// registered first+last. QRZ only: HamQTH's <nick> already serves that role
|
||||
// there, so the option would be a second switch for a behaviour it has.
|
||||
QRZPreferNickname bool `json:"qrz_prefer_nickname"`
|
||||
CacheTTLDays int `json:"cache_ttl_days"`
|
||||
}
|
||||
|
||||
@@ -2596,7 +2601,7 @@ func (a *App) reloadLookupProviders() {
|
||||
return
|
||||
}
|
||||
m, err := a.settings.GetMany(a.ctx,
|
||||
keyQRZUser, keyQRZPassword, keyHQUser, keyHQPassword,
|
||||
keyQRZUser, keyQRZPassword, keyQRZNickname, keyHQUser, keyHQPassword,
|
||||
keyCacheTTL, keyLookupPrimary, keyLookupFailsafe)
|
||||
if err != nil {
|
||||
fmt.Println("OpsLog: settings load error:", err)
|
||||
@@ -2610,7 +2615,9 @@ func (a *App) reloadLookupProviders() {
|
||||
switch name {
|
||||
case "qrz":
|
||||
if m[keyQRZUser] != "" && m[keyQRZPassword] != "" {
|
||||
return lookup.NewQRZ(m[keyQRZUser], m[keyQRZPassword])
|
||||
p := lookup.NewQRZ(m[keyQRZUser], m[keyQRZPassword])
|
||||
p.PreferNickname = m[keyQRZNickname] == "1"
|
||||
return p
|
||||
}
|
||||
case "hamqth":
|
||||
if m[keyHQUser] != "" && m[keyHQPassword] != "" {
|
||||
@@ -7198,7 +7205,7 @@ func (a *App) GetLookupSettings() (LookupSettings, error) {
|
||||
}
|
||||
m, err := a.settings.GetMany(a.ctx,
|
||||
keyQRZUser, keyQRZPassword, keyHQUser, keyHQPassword,
|
||||
keyCacheTTL, keyLookupPrimary, keyLookupFailsafe, keyLookupImages)
|
||||
keyCacheTTL, keyLookupPrimary, keyLookupFailsafe, keyLookupImages, keyQRZNickname)
|
||||
if err != nil {
|
||||
return LookupSettings{}, err
|
||||
}
|
||||
@@ -7214,6 +7221,7 @@ func (a *App) GetLookupSettings() (LookupSettings, error) {
|
||||
Primary: m[keyLookupPrimary],
|
||||
Failsafe: m[keyLookupFailsafe],
|
||||
DownloadImages: m[keyLookupImages] == "1",
|
||||
QRZPreferNickname: m[keyQRZNickname] == "1",
|
||||
CacheTTLDays: ttl,
|
||||
}, nil
|
||||
}
|
||||
@@ -7240,6 +7248,7 @@ func (a *App) SaveLookupSettings(s LookupSettings) error {
|
||||
keyLookupPrimary: s.Primary,
|
||||
keyLookupFailsafe: s.Failsafe,
|
||||
keyLookupImages: boolStr(s.DownloadImages),
|
||||
keyQRZNickname: boolStr(s.QRZPreferNickname),
|
||||
} {
|
||||
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||
return err
|
||||
|
||||
+4
-2
@@ -4,11 +4,13 @@
|
||||
"date": "",
|
||||
"en": [
|
||||
"Cluster: \"Hide worked\" no longer hides a spot that is a new prefix, county, grid or park in an entity already worked.",
|
||||
"DX Cluster: a spot lifetime can be set — 5, 10, 15 minutes or your own value — after which spots leave the list and the band maps."
|
||||
"DX Cluster: a spot lifetime can be set — 5, 10, 15 minutes or your own value — after which spots leave the list and the band maps.",
|
||||
"Call lookup: an option makes QRZ.com use the operator nickname as the name, falling back to the full name when none is published."
|
||||
],
|
||||
"fr": [
|
||||
"Cluster : « Masquer les contactés » ne masque plus un spot qui est un nouveau préfixe, comté, carré ou parc dans une contrée déjà faite.",
|
||||
"Cluster DX : on peut fixer une durée de vie des spots — 5, 10, 15 minutes ou une valeur libre — au-delà de laquelle ils quittent la liste et les band maps."
|
||||
"Cluster DX : on peut fixer une durée de vie des spots — 5, 10, 15 minutes ou une valeur libre — au-delà de laquelle ils quittent la liste et les band maps.",
|
||||
"Recherche d indicatif : une option fait utiliser à QRZ.com le surnom de l opérateur comme nom, avec repli sur le nom complet s il n y en a pas."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1224,6 +1224,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
hamqth_user: '', hamqth_password: '',
|
||||
primary: '', failsafe: '',
|
||||
download_images: false,
|
||||
qrz_prefer_nickname: false,
|
||||
cache_ttl_days: 30,
|
||||
});
|
||||
// Per-provider Test state — keeps the success/error feedback adjacent
|
||||
@@ -2312,6 +2313,23 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* QRZ only. HamQTH's <nick> already fills the Name field with the
|
||||
on-air name, so the same switch there would toggle a behaviour it
|
||||
has no way to turn off. */}
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer mt-3">
|
||||
<Checkbox
|
||||
checked={!!(lookup as any).qrz_prefer_nickname}
|
||||
onCheckedChange={(c) => setLookup((s) => ({ ...s, qrz_prefer_nickname: !!c } as any))}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span>
|
||||
{t('lk.qrzNickname')}
|
||||
<span className="block text-xs text-muted-foreground mt-0.5">
|
||||
{t('lk.qrzNicknameHint')}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-border">
|
||||
|
||||
@@ -243,7 +243,7 @@ const en: Dict = {
|
||||
'lk.provider': 'Provider', 'lk.primary': 'Primary', 'lk.failsafe': 'Failsafe', 'lk.user': 'User', 'lk.password': 'Password', 'lk.result': 'Result',
|
||||
'lk.testing': 'Testing…', 'lk.test': 'Test', 'lk.testTitle': "Run a sample lookup against the active profile's callsign to verify credentials",
|
||||
'lk.failsafeNote': 'Failsafe is consulted only when the Primary returns no match or errors. Set both to none (uncheck) during contests to skip the network entirely.',
|
||||
'lk.display': 'Display', 'lk.showPics': 'Show QRZ profile pictures',
|
||||
'lk.display': 'Display', 'lk.qrzNickname': 'QRZ.com: use the nickname as the name', 'lk.qrzNicknameHint': 'Logs the name the operator goes by on the air rather than their registered first and last name. Falls back to the full name when no nickname is published. QRZ.com only — HamQTH already does this.', 'lk.showPics': 'Show QRZ profile pictures',
|
||||
'lk.showPicsHint': 'Display the photo from QRZ.com next to the worked-before matrix. May noticeably slow lookups during busy contest days; turn off if you operate fast.',
|
||||
'lk.cache': 'Cache', 'lk.cacheHint': "Successful lookups are cached locally so the same callsign isn't fetched twice. TTL controls how long before a fresh query is made.",
|
||||
'lk.ttl': 'TTL (days)', 'lk.clearing': 'Clearing…', 'lk.clearCache': 'Clear cache now',
|
||||
@@ -666,7 +666,7 @@ const fr: Dict = {
|
||||
'lk.provider': 'Fournisseur', 'lk.primary': 'Principal', 'lk.failsafe': 'Secours', 'lk.user': 'Utilisateur', 'lk.password': 'Mot de passe', 'lk.result': 'Résultat',
|
||||
'lk.testing': 'Test…', 'lk.test': 'Test', 'lk.testTitle': "Lance une recherche test sur l'indicatif du profil actif pour vérifier les identifiants",
|
||||
'lk.failsafeNote': 'Le Secours n\'est consulté que si le Principal ne trouve rien ou échoue. Décoche les deux en contest pour éviter tout accès réseau.',
|
||||
'lk.display': 'Affichage', 'lk.showPics': 'Afficher les photos de profil QRZ',
|
||||
'lk.display': 'Affichage', 'lk.qrzNickname': 'QRZ.com : utiliser le surnom comme nom', 'lk.qrzNicknameHint': "Enregistre le nom sous lequel l'opérateur se présente à l'air plutôt que ses prénom et nom déclarés. Retombe sur le nom complet si aucun surnom n'est publié. QRZ.com uniquement — HamQTH le fait déjà.", 'lk.showPics': 'Afficher les photos de profil QRZ',
|
||||
'lk.showPicsHint': 'Affiche la photo de QRZ.com à côté de la matrice « déjà contacté ». Peut ralentir les recherches en contest ; désactive si tu opères vite.',
|
||||
'lk.cache': 'Cache', 'lk.cacheHint': "Les recherches réussies sont mises en cache localement pour ne pas re-interroger le même indicatif. Le TTL contrôle la durée avant une nouvelle requête.",
|
||||
'lk.ttl': 'TTL (jours)', 'lk.clearing': 'Effacement…', 'lk.clearCache': 'Vider le cache',
|
||||
|
||||
@@ -2541,6 +2541,7 @@ export namespace main {
|
||||
primary: string;
|
||||
failsafe: string;
|
||||
download_images: boolean;
|
||||
qrz_prefer_nickname: boolean;
|
||||
cache_ttl_days: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
@@ -2556,6 +2557,7 @@ export namespace main {
|
||||
this.primary = source["primary"];
|
||||
this.failsafe = source["failsafe"];
|
||||
this.download_images = source["download_images"];
|
||||
this.qrz_prefer_nickname = source["qrz_prefer_nickname"];
|
||||
this.cache_ttl_days = source["cache_ttl_days"];
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -21,6 +21,12 @@ type QRZ struct {
|
||||
|
||||
HTTP *http.Client
|
||||
|
||||
// PreferNickname takes QRZ's <nickname> over the composed first+last name
|
||||
// when the operator has published one. It is the name they go BY on the air,
|
||||
// which is what belongs in a log — HamQTH's <nick> is already used that way,
|
||||
// and this brings QRZ into line for operators who want it.
|
||||
PreferNickname bool
|
||||
|
||||
mu sync.Mutex
|
||||
session string
|
||||
loggedAt time.Time
|
||||
@@ -117,7 +123,7 @@ func (q *QRZ) fetch(ctx context.Context, sessionKey, callsign string) (Result, e
|
||||
}
|
||||
r := Result{
|
||||
Callsign: strings.ToUpper(c.Call),
|
||||
Name: joinName(c.FName, c.Name),
|
||||
Name: qrzName(q.PreferNickname, c.Nickname, c.FName, c.Name),
|
||||
QTH: c.Addr2,
|
||||
Address: composeQRZAddress(c.Addr1, c.Addr2, c.Zip, c.Country),
|
||||
State: strings.ToUpper(c.State),
|
||||
@@ -169,6 +175,7 @@ type qrzSession struct {
|
||||
type qrzCallsign struct {
|
||||
Call string `xml:"call"`
|
||||
FName string `xml:"fname"`
|
||||
Nickname string `xml:"nickname"` // the name the operator goes by on the air
|
||||
Name string `xml:"name"`
|
||||
Addr1 string `xml:"addr1"`
|
||||
Addr2 string `xml:"addr2"`
|
||||
@@ -235,3 +242,17 @@ func firstNonEmpty(s ...string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// qrzName picks what goes in the log's Name field.
|
||||
//
|
||||
// The nickname is only taken when the operator asked for it AND QRZ has one —
|
||||
// a blank nickname must never blank the name, which is the whole reason this is
|
||||
// a fallback rather than a swap.
|
||||
func qrzName(preferNickname bool, nickname, fname, name string) string {
|
||||
if preferNickname {
|
||||
if n := strings.TrimSpace(nickname); n != "" {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return joinName(fname, name)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package lookup
|
||||
|
||||
import "testing"
|
||||
|
||||
// The nickname is the name an operator goes BY on the air, and that is what
|
||||
// belongs in a log — "Bob", not "Robert J Smith". HamQTH's <nick> is already
|
||||
// used that way; this brings QRZ into line for operators who ask for it.
|
||||
//
|
||||
// The one thing it must never do is blank the name. A published nickname is
|
||||
// optional, so an empty one has to fall through to the registered name rather
|
||||
// than win by being "preferred".
|
||||
func TestQRZNamePrefersNicknameButFallsBack(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
prefer bool
|
||||
nickname, fname, name string
|
||||
want string
|
||||
}{
|
||||
{true, "Bob", "Robert", "Smith", "Bob"},
|
||||
{true, "", "Robert", "Smith", "Robert Smith"}, // no nickname published
|
||||
{true, " ", "Robert", "Smith", "Robert Smith"}, // blank is not a nickname
|
||||
{false, "Bob", "Robert", "Smith", "Robert Smith"}, // option off
|
||||
{true, "Bob", "", "", "Bob"},
|
||||
{false, "", "Robert", "", "Robert"},
|
||||
{true, "", "", "Smith", "Smith"},
|
||||
} {
|
||||
if got := qrzName(tc.prefer, tc.nickname, tc.fname, tc.name); got != tc.want {
|
||||
t.Errorf("qrzName(%v, %q, %q, %q) = %q, want %q",
|
||||
tc.prefer, tc.nickname, tc.fname, tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user