diff --git a/changelog.json b/changelog.json index a8e5dbe..e928c68 100644 --- a/changelog.json +++ b/changelog.json @@ -12,7 +12,8 @@ "Right-clicking QSOs offers HAMLOG.online among the upload targets, and the toast names each service properly instead of echoing its internal id.", "Bulk edit can set HAMLOG.online sent and received, each with its status and its date — the same four fields every other service has there. A log uploaded to their site by hand had no way to be marked as sent, so OpsLog kept offering to upload every contact again.", "HAMLOG.online joins the Confirmations defaults and the QSO grid columns — sent and received, each with its status and date, hidden by default like the other extras-backed columns.", - "Removed the explanatory paragraph from the Confirmations page." + "Removed the explanatory paragraph from the Confirmations page.", + "HAMLOG.online confirmations are read from the site's own ADIF field, APP_HAMLOG_QSO_CFM, taken from a real export. Import a log downloaded from HAMLOG and its confirmations count for awards straight away, with nothing to rename and no mapping to configure." ], "fr": [ "Yaesu : une ligne ANT dans le panneau du poste sélectionne la prise d'antenne, et le choix est mémorisé pour la bande sur laquelle il a été fait — on change de bande, l'antenne suit. Aucune page à configurer : choisir l'antenne une fois sur une bande le dit très bien. Les postes à une seule prise n'affichent jamais la ligne.", @@ -24,7 +25,8 @@ "Le clic droit sur des QSO propose HAMLOG.online parmi les destinations d'envoi, et la notification nomme correctement chaque service au lieu d'afficher son identifiant interne.", "L'édition en masse peut renseigner HAMLOG.online envoyé et reçu, chacun avec son statut et sa date — les mêmes quatre champs que les autres services. Un journal téléversé à la main sur leur site n'avait aucun moyen d'être marqué comme envoyé, et OpsLog proposait donc de renvoyer tous les contacts.", "HAMLOG.online rejoint les valeurs par défaut des Confirmations et les colonnes de la grille des QSO — envoyé et reçu, chacun avec son statut et sa date, masquées par défaut comme les autres colonnes issues des extras.", - "Suppression du paragraphe explicatif de la page Confirmations." + "Suppression du paragraphe explicatif de la page Confirmations.", + "Les confirmations HAMLOG.online sont lues dans le champ ADIF du site lui-même, APP_HAMLOG_QSO_CFM, relevé sur un export réel. Il suffit d'importer un journal téléchargé depuis HAMLOG pour que ses confirmations comptent aussitôt pour les diplômes, sans rien renommer ni configurer." ] }, { diff --git a/frontend/src/components/RecentQSOsGrid.tsx b/frontend/src/components/RecentQSOsGrid.tsx index fc6c806..60524a9 100644 --- a/frontend/src/components/RecentQSOsGrid.tsx +++ b/frontend/src/components/RecentQSOsGrid.tsx @@ -213,7 +213,7 @@ export const makeColCatalog = (t: TFn, myGrid?: string): ColEntry[] => [ // service, shown to everyone, is how a grid becomes unreadable. { group: 'QSL', label: t('rqg.c.hamlog_sent'), colId: 'hamlog_sent', headerName: t('rqg.h.hamlog_sent'), width: 100, cellClass: qslStatusCellClass, valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return e['APP_OPSLOG_HAMLOG_SENT'] || 'N'; }, defaultVisible: false }, { group: 'QSL', label: t('rqg.c.hamlog_sent_date'), colId: 'hamlog_sent_date', headerName: t('rqg.h.hamlog_sent_date'), width: 110, valueGetter: (p) => fmtDateOnly((p.data as any)?.extras?.['APP_OPSLOG_HAMLOG_SENT_DATE']), defaultVisible: false }, - { group: 'QSL', label: t('rqg.c.hamlog_rcvd'), colId: 'hamlog_rcvd', headerName: t('rqg.h.hamlog_rcvd'), width: 100, cellClass: qslStatusCellClass, valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return e['APP_OPSLOG_HAMLOG_QSL'] || 'N'; }, defaultVisible: false }, + { group: 'QSL', label: t('rqg.c.hamlog_rcvd'), colId: 'hamlog_rcvd', headerName: t('rqg.h.hamlog_rcvd'), width: 100, cellClass: qslStatusCellClass, valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return e['APP_HAMLOG_QSO_CFM'] || e['APP_OPSLOG_HAMLOG_QSL'] || 'N'; }, defaultVisible: false }, { group: 'QSL', label: t('rqg.c.hamlog_rcvd_date'), colId: 'hamlog_rcvd_date', headerName: t('rqg.h.hamlog_rcvd_date'), width: 110, valueGetter: (p) => fmtDateOnly((p.data as any)?.extras?.['APP_OPSLOG_HAMLOG_QSL_DATE']), defaultVisible: false }, // App-specific: when the QSO's audio recording was e-mailed to the station. { group: 'QSL', label: t('rqg.c.opslog_recording_sent'), colId: 'opslog_recording_sent', headerName: t('rqg.h.opslog_recording_sent'), width: 100, cellClass: qslStatusCellClass, valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return e['APP_OPSLOG_RECORDING_SENT'] ? 'Y' : 'N'; }, defaultVisible: false }, diff --git a/frontend/src/lib/rowColors.ts b/frontend/src/lib/rowColors.ts index fa31a11..4168537 100644 --- a/frontend/src/lib/rowColors.ts +++ b/frontend/src/lib/rowColors.ts @@ -42,7 +42,7 @@ const FIELDS: Record = { // internal/award/award.go, which reads the same keys for award confirmations. // One vocabulary, two readers. const HAMLOG_SENT = 'APP_OPSLOG_HAMLOG_SENT'; -const HAMLOG_RCVD = ['APP_OPSLOG_HAMLOG_QSL', 'APP_HAMLOG_QSL', 'APP_HAMLOGONLINE_QSL', 'HAMLOG_QSL_RCVD']; +const HAMLOG_RCVD = ['APP_HAMLOG_QSO_CFM', 'APP_OPSLOG_HAMLOG_QSL', 'APP_HAMLOG_QSL', 'APP_HAMLOGONLINE_QSL', 'HAMLOG_QSL_RCVD']; // hamlogState reads a row's extras. Any value that is not an explicit "no" // counts, because their export could carry a date or a match id rather than Y. diff --git a/internal/award/award.go b/internal/award/award.go index aec4ac0..aec538b 100644 --- a/internal/award/award.go +++ b/internal/award/award.go @@ -1502,18 +1502,23 @@ func confirmed(q *qso.QSO, sources []string, d *Def) bool { return false } -// HamlogQSLKey is where a HAMLOG.online confirmation is recorded, as an ADIF -// extras key. Exported and re-imported like any other extra, so the state -// survives a move to another logger and back. -const HamlogQSLKey = "APP_OPSLOG_HAMLOG_QSL" - -// hamlogAltKeys are the shapes an ADIF exported BY hamlog.online might use. +// HamlogQSLKey is where a HAMLOG.online confirmation is recorded. // -// Their site publishes no field name, so rather than demand that an operator -// rename a column by hand after every export, the ones an export could -// plausibly carry are accepted too. Costs three map lookups; saves a support -// thread that would end in "edit your ADIF". -var hamlogAltKeys = []string{"APP_HAMLOG_QSL", "APP_HAMLOGONLINE_QSL", "HAMLOG_QSL_RCVD"} +// THEIR field name, taken from a real export: +// +// Y +// +// Using the site's own key rather than one of ours is what makes an import +// simply work: a log downloaded from HAMLOG carries its confirmations into +// OpsLog with nothing to rename and no mapping to configure. The guesses this +// replaced (APP_HAMLOG_QSL and friends) were all wrong, which is the argument +// for reading a real file before naming a field. +const HamlogQSLKey = "APP_HAMLOG_QSO_CFM" + +// hamlogAltKeys are older or hand-written spellings still honoured on read. +// APP_OPSLOG_HAMLOG_QSL is the one OpsLog itself wrote before a real export was +// available; a log stamped with it keeps counting. +var hamlogAltKeys = []string{"APP_OPSLOG_HAMLOG_QSL", "APP_HAMLOG_QSL", "APP_HAMLOGONLINE_QSL", "HAMLOG_QSL_RCVD"} // hamlogConfirmed reports whether a QSO carries a HAMLOG.online confirmation. // diff --git a/internal/award/confirm_test.go b/internal/award/confirm_test.go index 86b5696..34a2197 100644 --- a/internal/award/confirm_test.go +++ b/internal/award/confirm_test.go @@ -95,3 +95,30 @@ func TestHamlogConfirmationSource(t *testing.T) { t.Error("a hamlog confirmation counted for an award that only accepts LoTW") } } + +// The field name is theirs, taken from a real export: +// +// Y +// +// Pinned as a test because it was GUESSED wrong first — an import that carries +// its confirmations into OpsLog with nothing to rename is the whole point, and +// a renamed constant would break it in silence. +func TestHamlogUsesTheSitesOwnFieldName(t *testing.T) { + if HamlogQSLKey != "APP_HAMLOG_QSO_CFM" { + t.Fatalf("HamlogQSLKey = %q — their export writes APP_HAMLOG_QSO_CFM", HamlogQSLKey) + } + def := Def{Confirm: []string{"hamlog"}} + // The record as HAMLOG exports it. + q := &qso.QSO{Extras: map[string]string{ + "APP_HAMLOG_R150COUNTRY": "Russia", + "APP_HAMLOG_QSO_CFM": "Y", + }} + if !Confirmed(q, def, def.Confirm) { + t.Error("a real HAMLOG export did not confirm") + } + // Anything OpsLog stamped before that export existed keeps counting. + old := &qso.QSO{Extras: map[string]string{"APP_OPSLOG_HAMLOG_QSL": "Y"}} + if !Confirmed(old, def, def.Confirm) { + t.Error("a QSO stamped with the older key stopped counting") + } +} diff --git a/internal/qso/bulkmode_test.go b/internal/qso/bulkmode_test.go index 67503b8..904d56f 100644 --- a/internal/qso/bulkmode_test.go +++ b/internal/qso/bulkmode_test.go @@ -33,7 +33,9 @@ func TestHamlogFieldsAreBulkEditable(t *testing.T) { for field, want := range map[string]string{ "hamlog_sent": "APP_OPSLOG_HAMLOG_SENT", "hamlog_sent_date": "APP_OPSLOG_HAMLOG_SENT_DATE", - "hamlog_rcvd": "APP_OPSLOG_HAMLOG_QSL", + // Their field name — see award.HamlogQSLKey. Editing it by hand and + // importing their ADIF must land in the SAME place. + "hamlog_rcvd": "APP_HAMLOG_QSO_CFM", "hamlog_rcvd_date": "APP_OPSLOG_HAMLOG_QSL_DATE", } { if got := BulkExtraKey(field); got != want { diff --git a/internal/qso/qso.go b/internal/qso/qso.go index 1431198..b041571 100644 --- a/internal/qso/qso.go +++ b/internal/qso/qso.go @@ -920,7 +920,7 @@ var bulkEditableExtras = map[string]string{ // operator asked of it. "hamlog_sent": "APP_OPSLOG_HAMLOG_SENT", "hamlog_sent_date": "APP_OPSLOG_HAMLOG_SENT_DATE", - "hamlog_rcvd": "APP_OPSLOG_HAMLOG_QSL", + "hamlog_rcvd": "APP_HAMLOG_QSO_CFM", "hamlog_rcvd_date": "APP_OPSLOG_HAMLOG_QSL_DATE", }