From 1e8708105819f18f5a36fab707352e033ddb8943 Mon Sep 17 00:00:00 2001 From: rouggy Date: Tue, 25 Aug 2026 19:30:29 +0200 Subject: [PATCH] feat(lists): a satellite list, offered on the entry form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SAT_NAME is compared character for character by the awards and by LoTW: AO-91 and AO91 are two different satellites to everything downstream, and typing it afresh on every pass is how one of them ends up in a log. So the station keeps its own list (Preferences → Lists → Satellites) and the field offers it, alphabetically — while still accepting anything typed, because a bird worked once and never added to the list must not be impossible to log. Not seeded with a shipped list of two dozen birds: an empty list means this station does not work satellites, and filling the dropdown with names nobody here has heard makes the field harder to use, not easier. Also fixes the RDA district comparison: its conflict list was capped at about six visible rows of a list holding up to two hundred, in a panel that would not scroll to show the rest, and nothing in it could be acted on. Taller, scrolling, and a callsign opens the contact. --- app.go | 60 +++++++++++++------ changelog.json | 8 ++- frontend/src/App.tsx | 1 + frontend/src/components/DetailsPanel.tsx | 25 +++++++- frontend/src/components/SettingsModal.tsx | 73 +++++++++++++++++++++-- frontend/src/lib/i18n.tsx | 8 +-- frontend/wailsjs/go/models.ts | 2 + 7 files changed, 145 insertions(+), 32 deletions(-) diff --git a/app.go b/app.go index cc1214c..a6653af 100644 --- a/app.go +++ b/app.go @@ -106,6 +106,7 @@ const ( keyListsRSTPhone = "lists.rst_phone" keyListsRSTCW = "lists.rst_cw" keyListsRSTDigital = "lists.rst_digital" + keyListsSatellites = "lists.satellites" // the satellites this station works (SAT_NAME dropdown) keyCATEnabled = "cat.enabled" keyCATBackend = "cat.backend" // "omnirig" | "flex" @@ -139,8 +140,8 @@ const ( // choice from whether a field happened to be filled in is how an operator // ends up with a host typed in and a radio that never answers. keyCATKenwoodLink = "cat.kenwood.link" - keyCATKenwoodPort = "cat.kenwood.port" // Kenwood CAT serial port (TS-590/890/2000, Elecraft) - keyCATKenwoodBaud = "cat.kenwood.baud" // Kenwood CAT baud (TS-590 default 9600, TS-890 115200) + keyCATKenwoodPort = "cat.kenwood.port" // Kenwood CAT serial port (TS-590/890/2000, Elecraft) + keyCATKenwoodBaud = "cat.kenwood.baud" // Kenwood CAT baud (TS-590 default 9600, TS-890 115200) // One key PER BACKEND, deliberately not shared. A Xiegu fix that reached // into the Yaesu and Kenwood backends is what broke them in 0.22.7/0.22.8; // the rule since is that nothing a backend does may be steered by another @@ -526,6 +527,12 @@ type ListsSettings struct { RSTPhone []string `json:"rst_phone"` // RS reports for phone modes RSTCW []string `json:"rst_cw"` // RST reports for CW/RTTY/PSK RSTDigital []string `json:"rst_digital"` // dB reports for FT8/FT4/JT… + // Satellites the station works, offered as a dropdown on the satellite + // fields. A list rather than free text because SAT_NAME is matched + // character for character by the awards and by LoTW: "AO-91" and "AO91" + // are two different satellites to everything downstream, and typing it + // afresh on every pass is how one of them appears in a log. + Satellites []string `json:"satellites"` } var defaultBands = []string{ @@ -8059,25 +8066,25 @@ func (a *App) GetCATSettings() (CATSettings, error) { return CATSettings{}, err } out := CATSettings{ - Enabled: m[keyCATEnabled] == "1", - Backend: m[keyCATBackend], - OmniRigNum: 1, - FlexHost: m[keyCATFlexHost], - FlexPort: 4992, - FlexSpots: m[keyCATFlexSpots] == "1", - FlexDVKDax: m[keyCATFlexDVKDax] == "1", - FlexDecodeSpots: m[keyCATFlexDecodeSpots] == "1", - FlexDecodeSecs: 120, - XieguPort: m[keyCATXieguPort], - XieguBaud: 19200, - XieguAddr: cat.XieguDefaultAddr, - YaesuPort: m[keyCATYaesuPort], - YaesuBaud: 38400, - KenwoodPort: m[keyCATKenwoodPort], - KenwoodHost: m[keyCATKenwoodHost], + Enabled: m[keyCATEnabled] == "1", + Backend: m[keyCATBackend], + OmniRigNum: 1, + FlexHost: m[keyCATFlexHost], + FlexPort: 4992, + FlexSpots: m[keyCATFlexSpots] == "1", + FlexDVKDax: m[keyCATFlexDVKDax] == "1", + FlexDecodeSpots: m[keyCATFlexDecodeSpots] == "1", + FlexDecodeSecs: 120, + XieguPort: m[keyCATXieguPort], + XieguBaud: 19200, + XieguAddr: cat.XieguDefaultAddr, + YaesuPort: m[keyCATYaesuPort], + YaesuBaud: 38400, + KenwoodPort: m[keyCATKenwoodPort], + KenwoodHost: m[keyCATKenwoodHost], // An install that predates the setting is read from what it has: a host // filled in meant the bridge, since that is what the old code preferred. - KenwoodLink: kenwoodLinkOr(m[keyCATKenwoodLink], m[keyCATKenwoodHost]), + KenwoodLink: kenwoodLinkOr(m[keyCATKenwoodLink], m[keyCATKenwoodHost]), KenwoodBaud: 9600, YaesuLowLines: m[keyCATYaesuLowLines] == "1", KenwoodLowLines: m[keyCATKenwoodLowLines] == "1", @@ -15405,6 +15412,9 @@ func (a *App) GetListsSettings() (ListsSettings, error) { if raw, _ := a.settings.Get(a.ctx, keyListsRSTDigital); raw != "" { _ = json.Unmarshal([]byte(raw), &out.RSTDigital) } + if raw, _ := a.settings.Get(a.ctx, keyListsSatellites); raw != "" { + _ = json.Unmarshal([]byte(raw), &out.Satellites) + } if len(out.Bands) == 0 { out.Bands = append([]string(nil), defaultBands...) } @@ -15420,6 +15430,10 @@ func (a *App) GetListsSettings() (ListsSettings, error) { if len(out.RSTDigital) == 0 { out.RSTDigital = append([]string(nil), defaultRSTDigital...) } + // Satellites are NOT defaulted to a shipped list. An empty list means the + // station does not work satellites, and filling it with two dozen birds + // nobody here has heard would make the field harder to use, not easier — + // the operator adds the ones they actually work. return out, nil } @@ -15459,6 +15473,14 @@ func (a *App) SaveListsSettings(l ListsSettings) error { // cache, so a band ticked here has to reach that cache now — otherwise it // takes effect at the next restart, which looks like the option not working. a.refreshChaseBands() + sat, err := json.Marshal(l.Satellites) + if err != nil { + return err + } + if err := a.settings.Set(a.ctx, keyListsSatellites, string(sat)); err != nil { + return err + } + return nil } diff --git a/changelog.json b/changelog.json index c17a1fb..d4f4cea 100644 --- a/changelog.json +++ b/changelog.json @@ -10,7 +10,9 @@ "PowerGenius XL: the amplifier's real state is read at startup. Its status frame carries no 'operate' field — the state is in 'state' — so on the direct GSCP link the flag was never read at all and OpsLog opened claiming STANDBY on an amp that was in line, with the first press of the button then commanding the state it was already in. IDLE means in line, not keyed.", "Antenna Genius: an option to write the SELECTED antenna into MY_ANTENNA, under the name it carries on the switch, ahead of the band default from Operating conditions. Which of the two ports counts is decided by the antenna jack the radio is transmitting on (ANT1/ANT2 on a Flex), with the jack-to-port wiring set once in Preferences — neither device can report it. When the port cannot be told, the log keeps the band default rather than naming an antenna at random.", "The awards tab beside the entry form (F3) now offers only the awards this station follows, and drops the ones switched off — the same list the Awards tab reads. It offered every award that existed, so a station chasing three of them picked references out of a list of twenty.", - "CAT settings ask two questions instead of one: WHICH RADIO, then HOW IT IS CONNECTED — and the second only appears where there is a choice to make. OmniRig leads the list, the brands follow alphabetically, and each offers only what it has: USB for a Yaesu or a Xiegu, USB or an RS-232-to-Ethernet bridge for a Kenwood or an Elecraft, USB or its own network protocol for an Icom, and nothing to choose for a FlexRadio or a SunSDR, which are reached one way each. The old list mixed the two questions — 'Icom (USB)' and 'Icom (network)' were separate entries while Kenwood and Elecraft hid the same choice in a field further down — and the example network address named port 4532, which is Hamlib rigctld and the one OpsLog itself serves under Share CAT." + "CAT settings ask two questions instead of one: WHICH RADIO, then HOW IT IS CONNECTED — and the second only appears where there is a choice to make. OmniRig leads the list, the brands follow alphabetically, and each offers only what it has: USB for a Yaesu or a Xiegu, USB or an RS-232-to-Ethernet bridge for a Kenwood or an Elecraft, USB or its own network protocol for an Icom, and nothing to choose for a FlexRadio or a SunSDR, which are reached one way each. The old list mixed the two questions — 'Icom (USB)' and 'Icom (network)' were separate entries while Kenwood and Elecraft hid the same choice in a field further down — and the example network address named port 4532, which is Hamlib rigctld and the one OpsLog itself serves under Share CAT.", + "An editable list of satellites (Preferences → Lists → Satellites). The satellite-name field on the entry form offers them as a dropdown, alphabetically, and still accepts anything typed. SAT_NAME is compared character for character by the awards and by LoTW — AO-91 and AO91 are two different satellites to everything downstream — so a remembered spelling beats one retyped on every pass.", + "Awards, RDA district comparison: the conflict list is taller and scrolls, and a callsign in it opens the contact. It was capped at about six visible rows of a list that holds up to two hundred, inside a panel that would not scroll to show the rest — and nothing in it could be acted on." ], "fr": [ "Console Elecraft : les mesures d'émission sont lues dès que la RADIO se déclare en émission, et plus seulement quand OpsLog l'a mise en émission. Passer en émission par le PTT de façade, une pédale ou le bouton du micro laissait le panneau croire à une réception — et comme les barres de puissance et de ROS ne sont lues qu'en émission, elles ne l'étaient jamais pour qui manipule à la main.", @@ -20,7 +22,9 @@ "Power Genius XL : l'état réel de l'amplificateur est lu au démarrage. Sa trame d'état ne contient pas de champ « operate » — l'état est dans « state » — si bien que sur la liaison GSCP directe l'indicateur n'était jamais lu : OpsLog s'ouvrait en annonçant STANDBY sur un ampli en ligne, et le premier appui commandait l'état dans lequel il se trouvait déjà. IDLE veut dire en ligne, pas en émission.", "Antenna Genius : une option pour inscrire l'antenne SÉLECTIONNÉE dans MY_ANTENNA, sous le nom qu'elle porte sur le switch, avant l'antenne par défaut des conditions de trafic. C'est la prise d'antenne sur laquelle la radio émet (ANT1/ANT2 sur un Flex) qui décide du port retenu, le câblage prise→port se règlant une fois dans les préférences — aucun des deux appareils ne peut le dire. Quand le port ne peut pas être déterminé, le journal conserve l'antenne par défaut plutôt que d'en nommer une au hasard.", "L'onglet des diplômes à côté de la saisie (F3) ne propose plus que les diplômes suivis par la station, et écarte ceux qui sont désactivés — la même liste que l'onglet Diplômes. Il proposait tous les diplômes existants : une station qui en chasse trois choisissait ses références dans une liste de vingt.", - "Les réglages CAT posent deux questions au lieu d'une : QUELLE RADIO, puis COMMENT ELLE EST RELIÉE — la seconde n'apparaissant que là où il y a un choix. OmniRig ouvre la liste, les marques suivent par ordre alphabétique, et chacune ne propose que ce qu'elle a : USB pour un Yaesu ou un Xiegu, USB ou un pont RS-232 vers Ethernet pour un Kenwood ou un Elecraft, USB ou son protocole réseau propre pour un Icom, et rien à choisir pour un FlexRadio ou un SunSDR, qui n'ont qu'une voie chacun. L'ancienne liste mélangeait les deux questions — « Icom (USB) » et « Icom (réseau) » étaient deux entrées tandis que Kenwood et Elecraft cachaient le même choix dans un champ plus bas — et l'exemple d'adresse réseau citait le port 4532, celui de Hamlib rigctld, que OpsLog propose lui-même sous « Partager le CAT »." + "Les réglages CAT posent deux questions au lieu d'une : QUELLE RADIO, puis COMMENT ELLE EST RELIÉE — la seconde n'apparaissant que là où il y a un choix. OmniRig ouvre la liste, les marques suivent par ordre alphabétique, et chacune ne propose que ce qu'elle a : USB pour un Yaesu ou un Xiegu, USB ou un pont RS-232 vers Ethernet pour un Kenwood ou un Elecraft, USB ou son protocole réseau propre pour un Icom, et rien à choisir pour un FlexRadio ou un SunSDR, qui n'ont qu'une voie chacun. L'ancienne liste mélangeait les deux questions — « Icom (USB) » et « Icom (réseau) » étaient deux entrées tandis que Kenwood et Elecraft cachaient le même choix dans un champ plus bas — et l'exemple d'adresse réseau citait le port 4532, celui de Hamlib rigctld, que OpsLog propose lui-même sous « Partager le CAT ».", + "Une liste de satellites éditable (Préférences → Listes → Satellites). Le champ du nom de satellite dans la saisie les propose en liste déroulante, par ordre alphabétique, et accepte toujours ce qu'on tape. SAT_NAME est comparé caractère par caractère par les diplômes et par LoTW — AO-91 et AO91 sont deux satellites différents pour tout ce qui suit — donc une orthographe mémorisée vaut mieux qu'une ressaisie à chaque passage.", + "Diplômes, comparaison des districts RDA : la liste des divergences est plus haute et défile, et un indicatif y ouvre le contact. Elle était limitée à six lignes visibles environ pour une liste pouvant en contenir deux cents, dans un panneau qui ne défilait pas pour montrer le reste — et rien n'y était actionnable." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8bd2f11..7be0dd5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8488,6 +8488,7 @@ export default function App() { {showSettings && ( { setShowSettings(false); setSettingsSection(undefined); refreshChaseNew(); }} onSaved={() => { diff --git a/frontend/src/components/DetailsPanel.tsx b/frontend/src/components/DetailsPanel.tsx index 79bbb5f..fa13163 100644 --- a/frontend/src/components/DetailsPanel.tsx +++ b/frontend/src/components/DetailsPanel.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { ComputeQSOAwardRefs, IsNewUSCounty } from '../../wailsjs/go/main/App'; +import { ComputeQSOAwardRefs, IsNewUSCounty, GetListsSettings } from '../../wailsjs/go/main/App'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; @@ -8,6 +8,7 @@ import { } from '@/components/ui/select'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; +import { Combobox } from '@/components/ui/combobox'; import { pathBetween, pathBetweenLatLon, gridToLatLon } from '@/lib/maidenhead'; import { BandSlotGrid } from '@/components/BandSlotGrid'; import { AwardRefSelector } from '@/components/AwardRefSelector'; @@ -153,6 +154,15 @@ function Field({ label, span = 1, className, children }: { label: string; span?: export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth, name, country, comment, note, details, onChange, wb, wbBusy, band, mode, bands, slotCall, slotBand, slotMode, slotWb, slotWbBusy, tab, onTab, keyerActive, onEditQso }: Props) { const { t } = useI18n(); const [internalOpen, setInternalOpen] = useState('stats'); + // The station's satellites, read once. Alphabetical because that is the only + // order a list of birds has, and an operator hunting AO-91 in a list sorted + // by when it was added is worse off than with a plain text box. + const [satellites, setSatellites] = useState([]); + useEffect(() => { + GetListsSettings() + .then((l: any) => setSatellites([...((l?.satellites ?? []) as string[])].filter(Boolean).sort())) + .catch(() => {}); + }, []); const open = tab ?? internalOpen; // controlled when `tab` is provided // Live award detection: run the SAME engine used at log time over the current @@ -478,7 +488,18 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth, {satelliteMode && ( <> - onChange({ sat_name: e.target.value })} /> + {/* The station's own satellites, in alphabetical order, with the + box still open to anything typed: SAT_NAME is compared + character for character by the awards and by LoTW, so a + remembered spelling beats a fresh one every pass — but a bird + worked once and never added to the list must not be + impossible to log. */} + onChange({ sat_name: v })} + /> onChange({ sat_mode: e.target.value })} /> diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index c764e71..3a864e9 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -175,6 +175,11 @@ interface Props { flexAvailable?: boolean; // CAT backend is FlexRadio → offer it as a Main pane icomAvailable?: boolean; // CAT backend is Icom → offer the Icom console as a Main pane yaesuAvailable?: boolean; // CAT backend is Yaesu → offer the Yaesu console as a Main pane + // Opens a QSO in the editor. Settings is not where a log is edited — but the + // RDA comparison lists contacts whose district is in dispute, and a list of + // things to fix that cannot be acted on is a list to write down and look up + // again later. + onEditQSO?: (id: number) => void; } // Pretty little card showing what OpsLog will stamp on each QSO based on @@ -200,6 +205,7 @@ type SectionId = | 'lookup' | 'lists-bands' | 'lists-modes' + | 'lists-satellites' | 'cluster' | 'backup' | 'database' @@ -310,6 +316,7 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[] { kind: 'group', label: t('nav.lists'), icon: Database, defaultOpen: true, children: [ { kind: 'item', label: t('sec.bands'), id: 'lists-bands' }, { kind: 'item', label: t('sec.modes'), id: 'lists-modes' }, + { kind: 'item', label: t('sec.satellites'), id: 'lists-satellites' }, ]}, { kind: 'item', label: t('sec.cluster'), id: 'cluster' }, { kind: 'item', label: t('sec.udp'), id: 'udp' }, @@ -339,7 +346,7 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[] // Map section id → i18n key (breadcrumb / placeholders). const SECTION_KEY: Partial> = { station: 'sec.station', profiles: 'sec.profiles', operating: 'sec.operating', confirmations: 'sec.confirmations', - 'external-services': 'sec.external', appearance: 'sec.appearance', lookup: 'sec.lookup', 'lists-bands': 'sec.bands', 'lists-modes': 'sec.modes', + 'external-services': 'sec.external', appearance: 'sec.appearance', lookup: 'sec.lookup', 'lists-bands': 'sec.bands', 'lists-modes': 'sec.modes', 'lists-satellites': 'sec.satellites', cluster: 'sec.cluster', backup: 'sec.backup', database: 'sec.database', autostart: 'sec.autostart', udp: 'sec.udp', adifmon: 'sec.adifmon', foldersync: 'sec.foldersync', @@ -361,6 +368,7 @@ const SECTION_LABELS: Partial> = { lookup: 'Callsign Lookup', 'lists-bands': 'Bands', 'lists-modes': 'Modes & default RST', + 'lists-satellites': 'Satellites', cluster: 'DX Cluster', backup: 'Database backup', database: 'Database', @@ -1516,7 +1524,7 @@ function brandOfBackend(backend: string, kenwoodLink?: string): { brand: string; } } -export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable, yaesuAvailable }: Props) { +export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable, yaesuAvailable, onEditQSO }: Props) { const { t } = useI18n(); const [selected, setSelected] = useState((initialSection as SectionId) || 'station'); const [loading, setLoading] = useState(true); @@ -1545,7 +1553,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan const [activeProfile, setActiveProfile] = useState(null); const updateActive = (patch: Partial) => setActiveProfile((p) => (p ? { ...p, ...patch } : p)); - const [lists, setLists] = useState({ bands: [], modes: [], rst_phone: [], rst_cw: [], rst_digital: [] }); + const [lists, setLists] = useState({ bands: [], modes: [], rst_phone: [], rst_cw: [], rst_digital: [], satellites: [] }); // RST report lists edited as free text (one/space-separated values). const [rstText, setRstText] = useState({ phone: '', cw: '', digital: '' }); // Custom band drafts (catalog covers ADIF spec but the user may have @@ -2931,6 +2939,40 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan ); } + function SatellitesPanel() { + const sats = lists.satellites ?? []; + return ( + <> + +
+
+ + {/* Raw text, one per line, parsed on change — not a row-per-entry + editor with add and delete buttons. The list is short, edited + twice a year, and usually arrives pasted from a satellite + tracker; a textarea takes that paste in one gesture. */} +