refactor(sat): one satellite list, and out of Hardware

Settings ▸ Lists ▸ Satellites is gone. It was a text box an operator
typed their birds into by hand, and it had nothing to do with the
satellites the tracker knew: the same station kept two lists of the same
satellites and they drifted apart. The SAT_NAME box on the entry form now
offers the followed set — or every satellite with a frequency plan when
none is followed — merged with whatever that old list still holds, read
and never written, because what it holds is somebody's past work.

And the satellite section moved out of Hardware, which it never was.
Which birds you chase, where your antenna stands and how old your
elements are is operating; the rotator inside it is the only hardware
there, and one block does not make a section a device.
This commit is contained in:
2026-09-07 15:41:56 +02:00
parent 3ed48336af
commit 9dfa6f7d39
7 changed files with 65 additions and 45 deletions
+5 -39
View File
@@ -214,7 +214,6 @@ type SectionId =
| 'lookup'
| 'lists-bands'
| 'lists-modes'
| 'lists-satellites'
| 'cluster'
| 'dxhunter'
| 'backup'
@@ -297,7 +296,6 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[]
const hardware: TreeNode[] = [
{ kind: 'item', label: t('sec.cat'), id: 'cat' },
{ kind: 'item', label: t('sec.rotator'), id: 'rotator' },
{ kind: 'item', label: t('sec.satellite'), id: 'satellite' },
{ kind: 'item', label: t('sec.winkeyer'), id: 'winkeyer' },
{ kind: 'item', label: t('sec.antenna'), id: 'antenna' },
{ kind: 'item', label: t('sec.antgenius'), id: 'antgenius', vendor: 'o3a' },
@@ -314,6 +312,10 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[]
{ kind: 'item', label: t('sec.station'), id: 'station' },
{ kind: 'item', label: t('sec.profiles'), id: 'profiles' },
{ kind: 'item', label: t('sec.operating'), id: 'operating' },
// Not hardware, whatever the rotator block inside it suggests: which
// satellites you chase, where your antenna stands and how old your
// elements are is operating, and it sits with the rest of it.
{ kind: 'item', label: t('sec.satellite'), id: 'satellite' },
{ kind: 'item', label: t('sec.confirmations'), id: 'confirmations' },
{ kind: 'item', label: t('sec.awards'), id: 'awards' },
{ kind: 'item', label: t('sec.external'), id: 'external-services' },
@@ -328,7 +330,6 @@ 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.dxhunter'), id: 'dxhunter' },
@@ -359,7 +360,7 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[]
// Map section id → i18n key (breadcrumb / placeholders).
const SECTION_KEY: Partial<Record<SectionId, string>> = {
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', 'lists-satellites': 'sec.satellites',
'external-services': 'sec.external', appearance: 'sec.appearance', lookup: 'sec.lookup', 'lists-bands': 'sec.bands', 'lists-modes': 'sec.modes',
cluster: 'sec.cluster', backup: 'sec.backup', database: 'sec.database', autostart: 'sec.autostart', udp: 'sec.udp',
adifmon: 'sec.adifmon',
foldersync: 'sec.foldersync',
@@ -381,7 +382,6 @@ const SECTION_LABELS: Partial<Record<SectionId, string>> = {
lookup: 'Callsign Lookup',
'lists-bands': 'Bands',
'lists-modes': 'Modes & default RST',
'lists-satellites': 'Satellites',
cluster: 'DX Cluster',
backup: 'Database backup',
database: 'Database',
@@ -3277,39 +3277,6 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
);
}
function SatellitesPanel() {
const sats = lists.satellites ?? [];
return (
<>
<SectionHeader title={t('sec.satellites')} hint={t('sat.hint')} />
<div className="space-y-3 max-w-xl">
<div className="space-y-1">
<Label>{t('sat.listLabel')}</Label>
{/* 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. */}
<textarea
className="w-full h-56 rounded-md border border-input bg-background p-2 font-mono text-xs"
value={sats.join('\n')}
placeholder={'AO-7\nAO-91\nRS-44\nSO-50'}
onChange={(e) => {
const next = e.target.value.split('\n').map((v) => v.trim());
setLists((s) => ({ ...s, satellites: next }));
}}
onBlur={() => setLists((s) => ({
// Tidied when the field is LEFT, never while typing: dropping an
// empty line as it is typed makes the Enter key look broken.
...s,
satellites: Array.from(new Set((s.satellites ?? []).map((v) => v.trim().toUpperCase()).filter(Boolean))).sort(),
}))}
/>
<p className="text-xs text-muted-foreground">{t('sat.listHint')}</p>
</div>
</div>
</>
);
}
function ModesPanel() {
const selected = lists.modes ?? [];
@@ -8675,7 +8642,6 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
lookup: LookupPanel,
'lists-bands': BandsPanel,
'lists-modes': ModesPanel,
'lists-satellites': SatellitesPanel,
cluster: ClusterPanel,
dxhunter: DXHunterPanel,
udp: UDPIntegrationsPanelWrapper,