fix(editor): a corrected frequency corrects its band

Band and frequency have to agree — the log, every award and every upload
are read on the BAND — and an operator fixing a wrong frequency is not
also expecting to fix the band by hand. That is exactly how a QSO ends
up filed on 20m at 7 MHz. Both sides follow, TX and RX.

Only when the number lands in a known allocation, because half a
frequency is typed on the way to all of it and a band must never be
blanked by that. bandForMHz moves out of App.tsx into lib/bandplan,
where the two callers can share one answer instead of drifting.
This commit is contained in:
2026-08-31 22:21:22 +02:00
parent e3332d1e27
commit 22e4266d38
4 changed files with 46 additions and 21 deletions
+4 -2
View File
@@ -7,14 +7,16 @@
"HamQTH: an “Upload the whole log” button in the QSL Manager — one file instead of one request per QSO, so a first sync takes seconds rather than the better part of an hour. It REPLACES the log held on HamQTH (the site has no partial upload), so it asks first, is scoped to the callsign this profile uploads as, and compresses a large log to stay under the 20 MB limit.",
"Outbound ADIF (forwarding a logged QSO to another logger such as Log4OM): the receive side is filled in when the contact was not split, so BAND_RX and FREQ_RX are present. The importer already did this; the logging paths did not, so what a QSO carried depended on which door it came in through.",
"HamQTH joins the places the other services already were: two Recent-QSOs columns (sent status and date), the QSO filter, and bulk edit — the last one so a log uploaded to HamQTH by hand can be marked as sent instead of being offered for upload all over again.",
"HamQTH whole-log upload: it reports itself in the console like every other action — the callsign it is scoped to, how many of the logbooks QSOs that leaves, the file size, and HamQTHs own reply — and says plainly that HamQTH imports the file in the background and e-mails any ADIF errors, so a site count that lags or stops short is explained rather than mysterious."
"HamQTH whole-log upload: it reports itself in the console like every other action — the callsign it is scoped to, how many of the logbooks QSOs that leaves, the file size, and HamQTHs own reply — and says plainly that HamQTH imports the file in the background and e-mails any ADIF errors, so a site count that lags or stops short is explained rather than mysterious.",
"QSO editor: correcting a frequency now moves its band with it, TX and RX — a QSO fixed to 7.1 MHz no longer stays filed on 20m. The band is only touched when the frequency lands in a known allocation, so a half-typed number never blanks it."
],
"fr": [
"DX Cluster : un serveur déconnecté garde sa pastille et peut donc être reconnecté — le déconnecter le faisait disparaître avec le seul moyen dy revenir.",
"HamQTH : un bouton « Envoyer tout le log » dans le QSL Manager — un seul fichier au lieu dune requête par QSO, une première synchro passe de près dune heure à quelques secondes. Il REMPLACE le log stocké sur HamQTH (le site na pas denvoi partiel) : il demande donc confirmation, se limite à lindicatif du profil et compresse un gros log pour rester sous la limite de 20 Mo.",
"ADIF sortant (transfert dun QSO vers un autre log, Log4OM par exemple) : le côté réception est renseigné quand le contact n’était pas en split, donc BAND_RX et FREQ_RX sont présents. Limport le faisait déjà, pas les chemins de log — ce quun QSO transportait dépendait donc de la porte par laquelle il était entré.",
"HamQTH rejoint les endroits où les autres services étaient déjà : deux colonnes dans les QSO récents (statut et date denvoi), le filtre de QSO et l’édition groupée — cette dernière pour quun log envoyé à la main sur HamQTH puisse être marqué comme envoyé au lieu d’être reproposé à lenvoi.",
"Envoi du log complet HamQTH : il rend compte dans la console comme toutes les autres actions — lindicatif retenu, combien de QSO du journal cela représente, la taille du fichier et la réponse de HamQTH — et indique clairement que HamQTH importe le fichier en arrière-plan et envoie les erreurs ADIF par e-mail : un compteur en retard ou incomplet sur le site est ainsi expliqué au lieu d’être mystérieux."
"Envoi du log complet HamQTH : il rend compte dans la console comme toutes les autres actions — lindicatif retenu, combien de QSO du journal cela représente, la taille du fichier et la réponse de HamQTH — et indique clairement que HamQTH importe le fichier en arrière-plan et envoie les erreurs ADIF par e-mail : un compteur en retard ou incomplet sur le site est ainsi expliqué au lieu d’être mystérieux.",
"Éditeur de QSO : corriger une fréquence déplace désormais sa bande avec elle, TX comme RX — un QSO corrigé à 7,1 MHz ne reste plus classé en 20m. La bande nest touchée que si la fréquence tombe dans une allocation connue : un nombre à moitié tapé ne lefface jamais."
]
},
{
+1 -15
View File
@@ -59,6 +59,7 @@ import { Combobox } from '@/components/ui/combobox';
import { applyAwardRefs, parseAwardRefs as parseManualRefs, spotRefList , withIOTARef, withRDARef } from '@/lib/awardRefs';
import { ListRadios, SetActiveRadio } from '../wailsjs/go/main/App';
import { formatDistance } from '@/lib/units';
import { bandForMHz } from '@/lib/bandplan';
import { EventsOn, BrowserOpenURL, WindowMinimise, WindowToggleMaximise, WindowIsMaximised, Quit } from '../wailsjs/runtime/runtime';
import type { adif as adifModels, lookup as lookupModels, cat as catModels } from '../wailsjs/go/models';
import type { QSOForm, WorkedBeforeView, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
@@ -432,21 +433,6 @@ function entryQSYCommand(text: string): { band?: string; hz?: number } | null {
return null;
}
function bandForMHz(mhz: number): string {
if (!mhz || isNaN(mhz)) return '';
const plan: [number, number, string][] = [
[1.8, 2.0, '160m'], [3.5, 4.0, '80m'], [5.06, 5.45, '60m'], [7.0, 7.3, '40m'],
[10.1, 10.15, '30m'], [14.0, 14.35, '20m'], [18.068, 18.168, '17m'], [21.0, 21.45, '15m'],
[24.89, 24.99, '12m'], [28.0, 29.7, '10m'], [50, 54, '6m'], [70, 71, '4m'],
[144, 148, '2m'], [222, 225, '1.25m'], [420, 450, '70cm'], [902, 928, '33cm'], [1240, 1300, '23cm'],
// Microwave, ADIF 3.1.7 ranges — kept in step with BandFromHz on the Go side.
[2300, 2450, '13cm'], [3300, 3500, '9cm'], [5650, 5925, '6cm'], [10000, 10500, '3cm'],
[24000, 24250, '1.25cm'], [47000, 47200, '6mm'], [75500, 81000, '4mm'],
[119980, 123000, '2.5mm'], [134000, 149000, '2mm'], [241000, 250000, '1mm'],
];
for (const [lo, hi, b] of plan) if (mhz >= lo && mhz <= hi) return b;
return '';
}
// modeAccent maps a mode to a theme-aware colour for the live-stations widget:
// CW gold, phone green, digital blue, unknown muted.
+18 -4
View File
@@ -22,6 +22,7 @@ import { Combobox } from '@/components/ui/combobox';
import { cn } from '@/lib/utils';
import { flagURL } from '@/lib/flags';
import { useI18n } from '@/lib/i18n';
import { bandForMHz } from '@/lib/bandplan';
import { titleCase, sentenceCase } from '@/lib/textCase';
import type { QSOForm } from '@/types';
@@ -312,6 +313,19 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
const splitHz = (hz?: number) => hz
? { khz: String(Math.floor(hz / 1000)), hz: String(hz % 1000).padStart(3, '0') }
: { khz: '', hz: '' };
// Correcting a frequency corrects its band. The pair has to agree — the log,
// every award and every upload are read on the BAND — and an operator fixing
// a wrong frequency is not also expecting to fix the band by hand, which is
// exactly how a QSO ends up filed on 20m at 7 MHz.
//
// Only when the number lands in a known allocation: half a frequency is typed
// on the way to all of it, and a band must never be blanked by that.
const syncBand = (khz: string, hz: string, field: 'band' | 'band_rx') => {
if (!khz.trim()) return;
const b = bandForMHz((parseInt(khz, 10) * 1000 + (parseInt(hz, 10) || 0)) / 1_000_000);
if (b) set(field, b as any);
};
const f0 = splitHz(draft.freq_hz);
const fr0 = splitHz(draft.freq_rx_hz);
const [freqKHz, setFreqKHz] = useState(f0.khz);
@@ -634,13 +648,13 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
</div>
<div className="flex items-center gap-2">
<Label className="w-20 shrink-0">{t('qedit.txFreq')}</Label>
<Input value={freqKHz} onChange={(e) => setFreqKHz(e.target.value.replace(/\D/g, ''))} className="font-mono w-24" placeholder="kHz" />
<Input value={freqHz} onChange={(e) => setFreqHz(e.target.value.replace(/\D/g, ''))} maxLength={3} className="font-mono w-16" placeholder="Hz" />
<Input value={freqKHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqKHz(v); syncBand(v, freqHz, 'band'); }} className="font-mono w-24" placeholder="kHz" />
<Input value={freqHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqHz(v); syncBand(freqKHz, v, 'band'); }} maxLength={3} className="font-mono w-16" placeholder="Hz" />
</div>
<div className="flex items-center gap-2">
<Label className="w-20 shrink-0">{t('qedit.rxFreq')}</Label>
<Input value={freqRxKHz} onChange={(e) => setFreqRxKHz(e.target.value.replace(/\D/g, ''))} className="font-mono w-24" placeholder="kHz" />
<Input value={freqRxHz} onChange={(e) => setFreqRxHz(e.target.value.replace(/\D/g, ''))} maxLength={3} className="font-mono w-16" placeholder="Hz" />
<Input value={freqRxKHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqRxKHz(v); syncBand(v, freqRxHz, 'band_rx'); }} className="font-mono w-24" placeholder="kHz" />
<Input value={freqRxHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqRxHz(v); syncBand(freqRxKHz, v, 'band_rx'); }} maxLength={3} className="font-mono w-16" placeholder="Hz" />
</div>
</div>
+23
View File
@@ -111,3 +111,26 @@ export function bandRange(band: string): [number, number] | undefined {
export function bandSegments(band: string): Seg[] {
return plan().segments[band] ?? [];
}
// bandForMHz maps a dial frequency (MHz) to its ADIF band, or '' when it falls
// outside every known allocation.
//
// Lives here rather than in a panel because more than one place has to answer
// the same question the same way: the entry form retunes on a typed frequency,
// and the QSO editor has to keep band and frequency agreeing when one of them
// is corrected. Kept in step with BandFromHz on the Go side.
export function bandForMHz(mhz: number): string {
if (!mhz || isNaN(mhz)) return '';
const plan: [number, number, string][] = [
[1.8, 2.0, '160m'], [3.5, 4.0, '80m'], [5.06, 5.45, '60m'], [7.0, 7.3, '40m'],
[10.1, 10.15, '30m'], [14.0, 14.35, '20m'], [18.068, 18.168, '17m'], [21.0, 21.45, '15m'],
[24.89, 24.99, '12m'], [28.0, 29.7, '10m'], [50, 54, '6m'], [70, 71, '4m'],
[144, 148, '2m'], [222, 225, '1.25m'], [420, 450, '70cm'], [902, 928, '33cm'], [1240, 1300, '23cm'],
// Microwave, ADIF 3.1.7 ranges.
[2300, 2450, '13cm'], [3300, 3500, '9cm'], [5650, 5925, '6cm'], [10000, 10500, '3cm'],
[24000, 24250, '1.25cm'], [47000, 47200, '6mm'], [75500, 81000, '4mm'],
[119980, 123000, '2.5mm'], [134000, 149000, '2mm'], [241000, 250000, '1mm'],
];
for (const [lo, hi, b] of plan) if (mhz >= lo && mhz <= hi) return b;
return '';
}