diff --git a/app.go b/app.go index c47d578..bbddb68 100644 --- a/app.go +++ b/app.go @@ -541,6 +541,8 @@ type StationSettings struct { MyCountry string `json:"my_country"` MySOTARef string `json:"my_sota_ref"` MyPOTARef string `json:"my_pota_ref"` + // MyIOTA is the island the station operates FROM (ADIF MY_IOTA), e.g. EU-005. + MyIOTA string `json:"my_iota"` } // LookupSettings is the JSON shape exchanged with the frontend. @@ -3033,6 +3035,9 @@ func (a *App) applyStationDefaults(q *qso.QSO, includeIdentity bool) { if q.MyPOTARef == "" { q.MyPOTARef = p.MyPOTARef } + if q.MyIOTA == "" { + q.MyIOTA = p.MyIOTA + } // MY_NAME = the operator's personal name (profile OpName, e.g. "Greg") — stamped // like the other My* fields so every QSO carries it, not just those edited by hand. if q.MyName == "" { @@ -13464,6 +13469,7 @@ func (a *App) GetStationSettings() (StationSettings, error) { MyCountry: p.MyCountry, MySOTARef: p.MySOTARef, MyPOTARef: p.MyPOTARef, + MyIOTA: p.MyIOTA, }, nil } @@ -13561,6 +13567,10 @@ func (a *App) SaveStationSettings(s StationSettings) error { p.MyCountry = s.MyCountry p.MySOTARef = s.MySOTARef p.MyPOTARef = s.MyPOTARef + // Uppercased on the way in: ADIF spells it EU-005, and an operator typing + // "eu-005" would otherwise put a reference no award matcher recognises on + // every QSO of an activation. + p.MyIOTA = strings.ToUpper(strings.TrimSpace(s.MyIOTA)) return a.profiles.Save(a.ctx, &p) } diff --git a/changelog.json b/changelog.json index 09931cf..5c7c27b 100644 --- a/changelog.json +++ b/changelog.json @@ -5,12 +5,14 @@ "en": [ "DX cluster: an L badge next to a callsign marks a station that uploads to LoTW, with a matching LoTW users only filter, and a Spotter continent filter narrows the list by where the spot came FROM — a JA report on 20 m tells a European little about their own path. The filter panel has been tidied along one rule: a switch is a behaviour you turn on or off, chips pick any number from a set. Nothing appears in both shapes any more, the Lock buttons sit in the heading of the section they lock, every section can be cleared the same way, and the whole panel is finally translated.", "DX cluster: the Spotter continent filter now actually matches. RBN skimmers report as VU2OY-# and cluster nodes as DL1ABC-2, and the suffix was passed straight to the prefix lookup, so every spot came back with no continent and the filter silently selected nothing.", - "A spot now stops being NEW the moment you log it. Logging announced the QSO but never dropped the cluster worked-index snapshot, so the refresh that follows re-read the answer computed BEFORE the contact — a station stayed yellow on the band map and in the cluster with the QSO plainly in the log, until something else happened to rebuild the index." + "A spot now stops being NEW the moment you log it. Logging announced the QSO but never dropped the cluster worked-index snapshot, so the refresh that follows re-read the answer computed BEFORE the contact — a station stayed yellow on the band map and in the cluster with the QSO plainly in the log, until something else happened to rebuild the index.", + "Station information gains an IOTA field (ADIF MY_IOTA, e.g. EU-005), stamped on every QSO like the SOTA and POTA references beside it. The QSO table has carried the field since the first release and both ADIF import and export already handled it — only the station profile could not supply it, so an island activation meant typing the reference on every contact." ], "fr": [ "Cluster DX : un badge L à côté de l indicatif signale une station qui utilise LoTW, avec le filtre Utilisateurs LoTW seulement qui va avec, et un filtre Continent du spotter restreint selon l origine du spot — un report JA sur 20 m dit peu de chose à un Européen sur son propre chemin. Le panneau de filtres a été remis d aplomb selon une seule règle : un interrupteur est un comportement qu on active ou non, les pastilles choisissent dans un ensemble. Plus rien n existe sous les deux formes, les boutons de verrouillage sont dans le titre de la section qu ils verrouillent, chaque section s efface de la même façon, et le panneau est enfin traduit.", "Cluster DX : le filtre Continent du spotter fonctionne enfin. Les skimmers RBN s annoncent en VU2OY-# et les nœuds cluster en DL1ABC-2, et ce suffixe partait tel quel dans la recherche de préfixe — tous les spots revenaient sans continent et le filtre ne sélectionnait rien.", - "Un spot cesse enfin d être NOUVEAU dès que tu l enregistres. La journalisation annonçait le QSO mais ne vidait jamais l instantané de l index des contacts, donc le rafraîchissement qui suit relisait la réponse calculée AVANT le contact — une station restait jaune sur le bandmap et dans le cluster alors que le QSO était bien au log, jusqu à ce qu autre chose reconstruise l index." + "Un spot cesse enfin d être NOUVEAU dès que tu l enregistres. La journalisation annonçait le QSO mais ne vidait jamais l instantané de l index des contacts, donc le rafraîchissement qui suit relisait la réponse calculée AVANT le contact — une station restait jaune sur le bandmap et dans le cluster alors que le QSO était bien au log, jusqu à ce qu autre chose reconstruise l index.", + "Les informations station gagnent un champ IOTA (MY_IOTA en ADIF, par exemple EU-005), estampillé sur chaque QSO comme les références SOTA et POTA à côté. La table des QSO portait le champ depuis la première version et l import comme l export ADIF le géraient déjà — seul le profil station ne savait pas le fournir, donc une activation d île obligeait à retaper la référence à chaque contact." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 521c05f..740a0a0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1933,7 +1933,7 @@ export default function App() { // === Station === const [station, setStation] = useState({ callsign: '', operator: '', - my_grid: '', my_country: '', my_sota_ref: '', my_pota_ref: '', + my_grid: '', my_country: '', my_sota_ref: '', my_pota_ref: '', my_iota: '', }); const [showFirstRun, setShowFirstRun] = useState(false); myCallRef.current = (station.callsign || '').toUpperCase(); diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 2ddaa08..0333c70 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -144,7 +144,7 @@ const emptyProfile = (): Profile => ({ my_grid: '', my_country: '', my_state: '', my_cnty: '', my_street: '', my_city: '', my_postal_code: '', - my_sota_ref: '', my_pota_ref: '', + my_sota_ref: '', my_pota_ref: '', my_iota: '', my_rig: '', my_antenna: '', tx_pwr: undefined, is_active: false, @@ -1854,6 +1854,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan my_grid: (activeProfile.my_grid ?? '').trim().toUpperCase(), my_sota_ref: (activeProfile.my_sota_ref ?? '').trim().toUpperCase(), my_pota_ref: (activeProfile.my_pota_ref ?? '').trim().toUpperCase(), + my_iota: (activeProfile.my_iota ?? '').trim().toUpperCase(), } as any); } await SaveLookupSettings(lookup as any); @@ -2013,6 +2014,10 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan updateActive({ my_pota_ref: e.target.value })} placeholder="FF-1234" /> +
+ + updateActive({ my_iota: e.target.value })} placeholder="EU-005" /> +
); diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index e44c51b..f9e9655 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -222,7 +222,7 @@ const en: Dict = { 'station.autofill': 'Auto-filled from the callsign — editable (stamped as MY_* on each QSO)', 'station.myGrid': 'My grid', 'station.myCountry': 'My country', 'station.dxcc': 'DXCC #', 'station.cqz': 'CQ zone', 'station.ituz': 'ITU zone', 'station.lat': 'Latitude', 'station.lon': 'Longitude', 'station.state': 'State / pref', 'station.county': 'County', - 'station.street': 'Street address', 'station.postal': 'Postal code', 'station.city': 'City', 'station.sota': 'SOTA ref', 'station.pota': 'POTA ref', + 'station.street': 'Street address', 'station.postal': 'Postal code', 'station.city': 'City', 'station.sota': 'SOTA ref', 'station.iota': 'IOTA (my island)', 'station.pota': 'POTA ref', // Confirmations panel 'conf.hint': 'Default QSL / eQSL / LoTW / upload status applied to every QSO you log — manually or via UDP auto-log from WSJT-X / JTDX / MSHV. Leave a field blank to keep the QSO column empty.', 'conf.sent': 'Sent', 'conf.rcvd': 'Rcvd', 'conf.paperQsl': 'Paper QSL', @@ -647,7 +647,7 @@ const fr: Dict = { 'station.autofill': "Auto-rempli depuis l'indicatif — éditable (inscrit en MY_* sur chaque QSO)", 'station.myGrid': 'Mon locator', 'station.myCountry': 'Mon pays', 'station.dxcc': 'DXCC #', 'station.cqz': 'Zone CQ', 'station.ituz': 'Zone ITU', 'station.lat': 'Latitude', 'station.lon': 'Longitude', 'station.state': 'État / préf', 'station.county': 'Comté', - 'station.street': 'Adresse', 'station.postal': 'Code postal', 'station.city': 'Ville', 'station.sota': 'Réf SOTA', 'station.pota': 'Réf POTA', + 'station.street': 'Adresse', 'station.postal': 'Code postal', 'station.city': 'Ville', 'station.sota': 'Réf SOTA', 'station.iota': 'IOTA (mon île)', 'station.pota': 'Réf POTA', 'conf.hint': 'Statut QSL / eQSL / LoTW / upload par défaut appliqué à chaque QSO enregistré — manuellement ou via auto-log UDP depuis WSJT-X / JTDX / MSHV. Laisse un champ vide pour garder la colonne du QSO vide.', 'conf.sent': 'Envoyée', 'conf.rcvd': 'Reçue', 'conf.paperQsl': 'QSL papier', 'conf.note': '« Envoyée » = statut d\'upload du QSO vers le service ; « N » est typique pour qu\'OpsLog suive les QSO restant à uploader. Club Log & HRDLog sont upload-only (pas de « reçue » en ADIF) ; QRZ.com a aussi « Reçue » = confirmé en retour.', diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index e85068f..8fb654a 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -3176,6 +3176,7 @@ export namespace main { my_country: string; my_sota_ref: string; my_pota_ref: string; + my_iota: string; static createFrom(source: any = {}) { return new StationSettings(source); @@ -3189,6 +3190,7 @@ export namespace main { this.my_country = source["my_country"]; this.my_sota_ref = source["my_sota_ref"]; this.my_pota_ref = source["my_pota_ref"]; + this.my_iota = source["my_iota"]; } } export class StationTestResult { @@ -3713,6 +3715,7 @@ export namespace profile { my_city: string; my_postal_code: string; my_sota_ref: string; + my_iota: string; my_pota_ref: string; my_rig: string; my_antenna: string; @@ -3750,6 +3753,7 @@ export namespace profile { this.my_city = source["my_city"]; this.my_postal_code = source["my_postal_code"]; this.my_sota_ref = source["my_sota_ref"]; + this.my_iota = source["my_iota"]; this.my_pota_ref = source["my_pota_ref"]; this.my_rig = source["my_rig"]; this.my_antenna = source["my_antenna"]; diff --git a/internal/db/migrations/0025_profile_iota.sql b/internal/db/migrations/0025_profile_iota.sql new file mode 100644 index 0000000..df39c4e --- /dev/null +++ b/internal/db/migrations/0025_profile_iota.sql @@ -0,0 +1,7 @@ +-- MY_IOTA: the island reference the station operates FROM, e.g. "EU-005". +-- +-- An official ADIF field, and the QSO table has carried my_iota since 0001 — +-- only the station profile could not supply it, so an island activation had to +-- have the reference typed on every contact or added afterwards. Blank by +-- default: the overwhelming majority of stations are not on an island. +ALTER TABLE station_profiles ADD COLUMN my_iota TEXT NOT NULL DEFAULT ''; diff --git a/internal/profile/profile.go b/internal/profile/profile.go index 98c8581..3cdb48c 100644 --- a/internal/profile/profile.go +++ b/internal/profile/profile.go @@ -52,6 +52,7 @@ type Profile struct { MyCity string `json:"my_city"` MyPostalCode string `json:"my_postal_code"` MySOTARef string `json:"my_sota_ref"` + MyIOTA string `json:"my_iota"` MyPOTARef string `json:"my_pota_ref"` MyRig string `json:"my_rig"` MyAntenna string `json:"my_antenna"` @@ -75,7 +76,7 @@ type Repo struct{ db *sql.DB } func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} } const selectCols = `id, name, callsign, operator, op_name, owner_callsign, my_grid, my_country, my_state, my_cnty, - my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref, + my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref, my_iota, my_rig, my_antenna, my_dxcc, my_cqz, my_ituz, my_lat, my_lon, tx_pwr, is_active, sort_order, db_config, created_at, updated_at` @@ -124,12 +125,12 @@ func (r *Repo) Save(ctx context.Context, p *Profile) error { res, err := r.db.ExecContext(ctx, ` INSERT INTO station_profiles (name, callsign, operator, op_name, owner_callsign, my_grid, my_country, my_state, my_cnty, - my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref, + my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref, my_iota, my_rig, my_antenna, my_dxcc, my_cqz, my_ituz, my_lat, my_lon, tx_pwr, is_active, sort_order, created_at, updated_at) - VALUES(?,?,?,?,?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?,?,?,?, ?,?,?,?)`, + VALUES(?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?,?,?, ?,?,?,?)`, p.Name, p.Callsign, p.Operator, p.OpName, p.OwnerCallsign, p.MyGrid, p.MyCountry, p.MyState, p.MyCounty, - p.MyStreet, p.MyCity, p.MyPostalCode, p.MySOTARef, p.MyPOTARef, + p.MyStreet, p.MyCity, p.MyPostalCode, p.MySOTARef, p.MyPOTARef, p.MyIOTA, p.MyRig, p.MyAntenna, nullableInt(p.MyDXCC), nullableInt(p.MyCQZone), nullableInt(p.MyITUZone), nullableFloat(p.MyLat), nullableFloat(p.MyLon), nullableFloat(p.TxPower), boolInt(p.IsActive), p.SortOrder, now, now) @@ -144,13 +145,13 @@ func (r *Repo) Save(ctx context.Context, p *Profile) error { UPDATE station_profiles SET name = ?, callsign = ?, operator = ?, op_name = ?, owner_callsign = ?, my_grid = ?, my_country = ?, my_state = ?, my_cnty = ?, my_street = ?, my_city = ?, my_postal_code = ?, - my_sota_ref = ?, my_pota_ref = ?, my_rig = ?, my_antenna = ?, + my_sota_ref = ?, my_pota_ref = ?, my_iota = ?, my_rig = ?, my_antenna = ?, my_dxcc = ?, my_cqz = ?, my_ituz = ?, my_lat = ?, my_lon = ?, tx_pwr = ?, sort_order = ?, updated_at = ? WHERE id = ?`, p.Name, p.Callsign, p.Operator, p.OpName, p.OwnerCallsign, p.MyGrid, p.MyCountry, p.MyState, p.MyCounty, p.MyStreet, p.MyCity, p.MyPostalCode, - p.MySOTARef, p.MyPOTARef, p.MyRig, p.MyAntenna, + p.MySOTARef, p.MyPOTARef, p.MyIOTA, p.MyRig, p.MyAntenna, nullableInt(p.MyDXCC), nullableInt(p.MyCQZone), nullableInt(p.MyITUZone), nullableFloat(p.MyLat), nullableFloat(p.MyLon), nullableFloat(p.TxPower), p.SortOrder, now, p.ID) @@ -264,7 +265,7 @@ func scan(row scannable) (Profile, error) { var p Profile var ( callsign, operator, opName, ownerCall, myGrid, myCountry, myState, myCnty, - myStreet, myCity, myPostal, mySOTA, myPOTA, + myStreet, myCity, myPostal, mySOTA, myPOTA, myIOTA, myRig, myAntenna sql.NullString myDXCC, myCQZ, myITUZ sql.NullInt64 myLat, myLon, txPwr sql.NullFloat64 @@ -273,7 +274,7 @@ func scan(row scannable) (Profile, error) { createdAt, updatedAt string ) err := row.Scan(&p.ID, &p.Name, &callsign, &operator, &opName, &ownerCall, &myGrid, &myCountry, &myState, &myCnty, - &myStreet, &myCity, &myPostal, &mySOTA, &myPOTA, + &myStreet, &myCity, &myPostal, &mySOTA, &myPOTA, &myIOTA, &myRig, &myAntenna, &myDXCC, &myCQZ, &myITUZ, &myLat, &myLon, &txPwr, &isActive, &sortOrder, &dbConfig, &createdAt, &updatedAt) if err != nil { @@ -294,6 +295,7 @@ func scan(row scannable) (Profile, error) { p.MyCity = myCity.String p.MyPostalCode = myPostal.String p.MySOTARef = mySOTA.String + p.MyIOTA = myIOTA.String p.MyPOTARef = myPOTA.String p.MyRig = myRig.String p.MyAntenna = myAntenna.String