diff --git a/changelog.json b/changelog.json index f53ac7e..1c8d32e 100644 --- a/changelog.json +++ b/changelog.json @@ -1,4 +1,14 @@ [ + { + "version": "0.21.9", + "date": "2026-07-28", + "en": [ + "CQ and ITU zones you correct by hand in your profile stay corrected. They are derived from cty.dat, which gives the zones of the whole entity — in a country spanning several, the automatic value is wrong and it came back at every restart. They now only fill in when empty, and recompute when the callsign or grid changes." + ], + "fr": [ + "Les zones CQ et ITU corrigées à la main dans votre profil restent corrigées. Elles proviennent de cty.dat, qui donne les zones de l'entité entière — dans un pays qui en couvre plusieurs, la valeur automatique est fausse et revenait à chaque redémarrage. Elles ne se remplissent désormais que si le champ est vide, et se recalculent quand l'indicatif ou le locator change." + ] + }, { "version": "0.21.8", "date": "2026-07-28", diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index d4991de..0c2cb2d 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -1483,27 +1483,49 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan return () => { off(); }; }, []); + // Which (profile, callsign, grid) the auto-fill below last derived from. + // Without it, simply OPENING the settings counted as "the source changed" and + // overwrote the operator's own values. + const derivedFrom = useRef(''); + // Auto-fill the active profile's MY_* DXCC metadata from the station - // callsign (country, DXCC#, CQ/ITU zones) and the grid (lat/lon). These - // are derived values, so they always recompute when the callsign or grid - // changes — the user can still edit a field, it just re-populates when the - // source changes. Debounced so we don't hammer cty.dat while typing. + // callsign (country, DXCC#, CQ/ITU zones) and the grid (lat/lon). + // + // These are derived values, so they recompute when the callsign or grid + // changes. What they must NOT do is recompute on a plain load: cty.dat gives + // the zones of the ENTITY, and a large country spans several — an operator in + // CQ 14 / ITU 27 was handed 15 and 28 and corrected them by hand, and every + // restart put the wrong pair back. cty.dat is a starting point, not an + // authority, so on a load we only fill fields that are EMPTY; a value the + // operator typed stays until the callsign or grid itself changes. + // Debounced so we don't hammer cty.dat while typing. useEffect(() => { const call = (activeProfile?.callsign ?? '').trim(); if (!call) return; const grid = (activeProfile?.my_grid ?? '').trim(); + const source = `${activeProfile?.id ?? 0}|${call.toUpperCase()}|${grid.toUpperCase()}`; + // First sight of this profile — a load, not an edit. + const isLoad = derivedFrom.current === '' || derivedFrom.current.split('|')[0] !== String(activeProfile?.id ?? 0); const t = window.setTimeout(async () => { try { const i: any = await ComputeStationInfo(call, grid); + derivedFrom.current = source; setActiveProfile((p) => { if (!p) return p; const patch: any = {}; - if (i.country) patch.my_country = i.country; - if (i.dxcc) patch.my_dxcc = i.dxcc; - if (i.cqz) patch.my_cqz = i.cqz; - if (i.ituz) patch.my_ituz = i.ituz; - if (i.lat) patch.my_lat = i.lat; - if (i.lon) patch.my_lon = i.lon; + // On a load, keep in patch only what the profile does not already have. + const put = (k: string, v: any) => { + if (!v) return; + const cur = (p as any)[k]; + if (isLoad && cur !== undefined && cur !== null && cur !== '' && cur !== 0) return; + patch[k] = v; + }; + put('my_country', i.country); + put('my_dxcc', i.dxcc); + put('my_cqz', i.cqz); + put('my_ituz', i.ituz); + put('my_lat', i.lat); + put('my_lon', i.lon); // Only re-render when a value actually changed (prevents loops). const changed = Object.keys(patch).some((k) => (p as any)[k] !== patch[k]); return changed ? { ...p, ...patch } : p;