feat: Added support for US Counties in OpsLog / Extra feature with DXHunter
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
||||
GetPOTAToken, SavePOTAToken,
|
||||
TestLoTWUpload, ListTQSLStationLocations,
|
||||
DownloadLoTWUsers, GetLoTWUsersStatus,
|
||||
DownloadULSCounties, ULSStatus, BackfillUSCounties,
|
||||
ComputeStationInfo,
|
||||
GetUIPref, SetUIPref,
|
||||
GetFlexState, GetFlexBandAntennas, SaveFlexBandAntennas,
|
||||
@@ -175,6 +176,7 @@ type SectionId =
|
||||
| 'backup'
|
||||
| 'database'
|
||||
| 'autostart'
|
||||
| 'uscounties'
|
||||
| 'awards'
|
||||
| 'cat'
|
||||
| 'rotator'
|
||||
@@ -223,6 +225,7 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[]
|
||||
]},
|
||||
{ kind: 'item', label: t('sec.cluster'), id: 'cluster' },
|
||||
{ kind: 'item', label: t('sec.udp'), id: 'udp' },
|
||||
{ kind: 'item', label: t('sec.uscounties'), id: 'uscounties' },
|
||||
{ kind: 'item', label: t('sec.database'), id: 'database' },
|
||||
{ kind: 'item', label: t('sec.autostart'), id: 'autostart' },
|
||||
],
|
||||
@@ -238,6 +241,7 @@ const SECTION_KEY: Partial<Record<SectionId, string>> = {
|
||||
station: 'sec.station', profiles: 'sec.profiles', operating: 'sec.operating', confirmations: 'sec.confirmations',
|
||||
'external-services': 'sec.external', 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',
|
||||
uscounties: 'sec.uscounties',
|
||||
awards: 'sec.awards', cat: 'sec.cat', rotator: 'sec.rotator', winkeyer: 'sec.winkeyer', antenna: 'sec.antenna',
|
||||
antgenius: 'sec.antgenius', pgxl: 'sec.pgxl', flex: 'sec.flex', audio: 'sec.audio', general: 'sec.general', email: 'sec.email',
|
||||
};
|
||||
@@ -1010,6 +1014,34 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
catch (e: any) { setLotwTest({ ok: false, msg: String(e?.message ?? e) }); }
|
||||
finally { setLotwUsersBusy(false); }
|
||||
};
|
||||
// US Counties (offline FCC ULS) — download progress arrives via events.
|
||||
const [ulsStatus, setUlsStatus] = useState<{ count: number; updated_at?: string }>({ count: 0 });
|
||||
const [ulsBusy, setUlsBusy] = useState(false);
|
||||
const [ulsProgress, setUlsProgress] = useState<{ stage: string; pct: number } | null>(null);
|
||||
const [ulsMsg, setUlsMsg] = useState<{ ok: boolean; text: string } | null>(null);
|
||||
const [backfillBusy, setBackfillBusy] = useState(false);
|
||||
const [backfillMsg, setBackfillMsg] = useState<string | null>(null);
|
||||
useEffect(() => { ULSStatus().then((s) => setUlsStatus(s as any)).catch(() => {}); }, []);
|
||||
useEffect(() => {
|
||||
const off1 = EventsOn('uls:progress', (p: any) => setUlsProgress({ stage: p?.stage ?? '', pct: p?.pct ?? 0 }));
|
||||
const off2 = EventsOn('uls:done', async (r: any) => {
|
||||
setUlsBusy(false); setUlsProgress(null);
|
||||
setUlsMsg(r?.ok ? { ok: true, text: t('uscty.done', { n: r?.count ?? 0 }) } : { ok: false, text: r?.error || 'error' });
|
||||
try { const s = await ULSStatus(); setUlsStatus(s as any); } catch {}
|
||||
});
|
||||
return () => { off1(); off2(); };
|
||||
}, []);
|
||||
const downloadUls = async () => {
|
||||
setUlsBusy(true); setUlsMsg(null); setUlsProgress({ stage: '', pct: 0 });
|
||||
try { await DownloadULSCounties(); }
|
||||
catch (e: any) { setUlsBusy(false); setUlsProgress(null); setUlsMsg({ ok: false, text: String(e?.message ?? e) }); }
|
||||
};
|
||||
const runBackfill = async () => {
|
||||
setBackfillBusy(true); setBackfillMsg(null);
|
||||
try { const r: any = await BackfillUSCounties(); setBackfillMsg(t('uscty.backfillDone', { c: r?.county ?? 0, g: r?.grid ?? 0, s: r?.scanned ?? 0 })); }
|
||||
catch (e: any) { setBackfillMsg(String(e?.message ?? e)); }
|
||||
finally { setBackfillBusy(false); }
|
||||
};
|
||||
const [hrdlogTest, setHrdlogTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [hrdlogTesting, setHrdlogTesting] = useState(false);
|
||||
const [eqslTest, setEqslTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
@@ -4332,6 +4364,68 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
);
|
||||
}
|
||||
|
||||
function USCountiesPanel() {
|
||||
const loaded = ulsStatus.count > 0;
|
||||
return (
|
||||
<div className="max-w-2xl space-y-5">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-1">{t('uscty.title')}</h3>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">{t('uscty.intro')}</p>
|
||||
</div>
|
||||
|
||||
{/* Required-download notice */}
|
||||
<div className="rounded-md border border-warning/40 bg-warning/10 p-3 text-xs text-foreground/90 leading-relaxed">
|
||||
{t('uscty.needDownload')}
|
||||
</div>
|
||||
|
||||
{/* Status + download */}
|
||||
<div className="rounded-md border border-border p-3 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-xs">
|
||||
<div className="font-medium">{t('uscty.dbStatus')}</div>
|
||||
<div className="text-muted-foreground">
|
||||
{loaded
|
||||
? t('uscty.loaded', { n: ulsStatus.count.toLocaleString(), date: ulsStatus.updated_at ? new Date(ulsStatus.updated_at).toLocaleDateString() : '—' })
|
||||
: t('uscty.notLoaded')}
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" onClick={downloadUls} disabled={ulsBusy}>
|
||||
{ulsBusy ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : <ArrowDown className="size-3.5 mr-1.5" />}
|
||||
{loaded ? t('uscty.update') : t('uscty.download')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{ulsProgress && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-[11px] text-muted-foreground flex justify-between">
|
||||
<span>{ulsProgress.stage}</span><span>{ulsProgress.pct}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full rounded bg-muted overflow-hidden">
|
||||
<div className="h-full bg-primary transition-all" style={{ width: `${ulsProgress.pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{ulsMsg && (
|
||||
<div className={cn('text-xs', ulsMsg.ok ? 'text-success' : 'text-destructive')}>{ulsMsg.text}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Backfill existing QSOs */}
|
||||
<div className="rounded-md border border-border p-3 space-y-2">
|
||||
<div className="text-xs font-medium">{t('uscty.backfillTitle')}</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">{t('uscty.backfillIntro')}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button size="sm" variant="secondary" onClick={runBackfill} disabled={backfillBusy || !loaded}>
|
||||
{backfillBusy ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : null}
|
||||
{t('uscty.backfillRun')}
|
||||
</Button>
|
||||
{backfillMsg && <span className="text-xs text-muted-foreground">{backfillMsg}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Map sections to their content + icon (for placeholder).
|
||||
const PANELS: Record<SectionId, () => JSX.Element> = {
|
||||
general: GeneralPanel,
|
||||
@@ -4348,6 +4442,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
udp: UDPIntegrationsPanelWrapper,
|
||||
backup: BackupPanel,
|
||||
database: DatabasePanel,
|
||||
uscounties: USCountiesPanel,
|
||||
autostart: () => <AutostartPanelComponent />,
|
||||
awards: () => <ComingSoon id="awards" icon={Award} />,
|
||||
cat: CATPanel,
|
||||
|
||||
@@ -89,7 +89,21 @@ const en: Dict = {
|
||||
'sec.confirmations': 'Confirmations', 'sec.external': 'External services',
|
||||
'sec.general': 'General', 'sec.email': 'E-mail (SMTP)', 'sec.lookup': 'Callsign Lookup',
|
||||
'sec.bands': 'Bands', 'sec.modes': 'Modes & default RST', 'sec.cluster': 'DX Cluster',
|
||||
'sec.udp': 'UDP integrations', 'sec.database': 'Database', 'sec.autostart': 'Autostart', 'sec.backup': 'Database backup', 'station.title': 'Station Control', 'station.rotator': 'Rotator', 'station.rotatorNoRead': 'No heading read', 'station.pattern': 'Pattern', 'station.bi': 'Bi', 'station.retract': 'Retract elements', 'station.moving': 'MOVING', 'station.elements': 'Elements (mm)', 'station.read': 'Read', 'station.readLengths': 'Read current element lengths from the controller', 'station.noLengths': 'Lengths unknown — click Read to fetch them from the controller.', 'station.element': 'Element', 'station.reflector': 'Reflector', 'station.driven': 'Driven', 'station.director': 'Dir', 'station.set': 'Set', 'station.elementsHint': 'Each press lengthens/shortens the element by 2 mm (like the physical console). Verify which element responds on your antenna.', 'station.go': 'Go', 'station.stop': 'Stop', 'station.dragHint': 'Drag a widget by its card to reorder. Pick a column count to lay them out in a grid.', 'station.colsAuto': 'Auto', 'station.addDevice': 'Add device', 'station.editDevice': 'Edit device', 'station.empty': 'No relay boards yet. Add a WebSwitch 1216H or a KMTronic 8-relay board to control your station power and accessories.', 'station.online': 'Online', 'station.offline': 'Offline', 'station.edit': 'Edit', 'station.delete': 'Delete', 'station.relay': 'Relay', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': 'Device type', 'station.name': 'Name', 'station.host': 'Host / IP', 'station.user': 'Username', 'station.pass': 'Password', 'station.optional': 'optional', 'station.labels': 'Relay labels', 'station.cancel': 'Cancel', 'station.save': 'Save',
|
||||
'sec.udp': 'UDP integrations', 'sec.database': 'Database', 'sec.autostart': 'Autostart', 'sec.backup': 'Database backup', 'sec.uscounties': 'US Counties',
|
||||
'uscty.title': 'US Counties (USA-CA)',
|
||||
'uscty.intro': 'Resolve a US callsign to its county and grid offline, from the FCC ULS licence database. This powers the US Counties award and county hunting — including on CW/SSB, where a spot carries only a callsign.',
|
||||
'uscty.needDownload': 'County resolution requires downloading the FCC database first (about 150 MB, stored locally). Nothing is resolved until you download it.',
|
||||
'uscty.dbStatus': 'County database',
|
||||
'uscty.loaded': '{n} callsigns · updated {date}',
|
||||
'uscty.notLoaded': 'Not downloaded yet.',
|
||||
'uscty.download': 'Download',
|
||||
'uscty.update': 'Update',
|
||||
'uscty.done': 'County database ready — {n} callsigns.',
|
||||
'uscty.backfillTitle': 'Fill existing QSOs',
|
||||
'uscty.backfillIntro': 'Resolve county (and grid) for US QSOs already in your log that are missing them. Existing values are kept — only blanks are filled.',
|
||||
'uscty.backfillRun': 'Fill missing counties',
|
||||
'uscty.backfillDone': '{s} US QSOs scanned · {c} counties, {g} grids filled.',
|
||||
'station.title': 'Station Control', 'station.rotator': 'Rotator', 'station.rotatorNoRead': 'No heading read', 'station.pattern': 'Pattern', 'station.bi': 'Bi', 'station.retract': 'Retract elements', 'station.moving': 'MOVING', 'station.elements': 'Elements (mm)', 'station.read': 'Read', 'station.readLengths': 'Read current element lengths from the controller', 'station.noLengths': 'Lengths unknown — click Read to fetch them from the controller.', 'station.element': 'Element', 'station.reflector': 'Reflector', 'station.driven': 'Driven', 'station.director': 'Dir', 'station.set': 'Set', 'station.elementsHint': 'Each press lengthens/shortens the element by 2 mm (like the physical console). Verify which element responds on your antenna.', 'station.go': 'Go', 'station.stop': 'Stop', 'station.dragHint': 'Drag a widget by its card to reorder. Pick a column count to lay them out in a grid.', 'station.colsAuto': 'Auto', 'station.addDevice': 'Add device', 'station.editDevice': 'Edit device', 'station.empty': 'No relay boards yet. Add a WebSwitch 1216H or a KMTronic 8-relay board to control your station power and accessories.', 'station.online': 'Online', 'station.offline': 'Offline', 'station.edit': 'Edit', 'station.delete': 'Delete', 'station.relay': 'Relay', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': 'Device type', 'station.name': 'Name', 'station.host': 'Host / IP', 'station.user': 'Username', 'station.pass': 'Password', 'station.optional': 'optional', 'station.labels': 'Relay labels', 'station.cancel': 'Cancel', 'station.save': 'Save',
|
||||
'sec.awards': 'Awards', 'sec.cat': 'CAT interface', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'CW Keyer',
|
||||
'sec.antenna': 'Ultrabeam / Steppir', 'sec.antgenius': 'Antenna Genius', 'sec.pgxl': 'Power Genius', 'sec.flex': 'FlexRadio', 'sec.audio': 'Audio devices',
|
||||
// General panel
|
||||
@@ -353,7 +367,21 @@ const fr: Dict = {
|
||||
'sec.confirmations': 'Confirmations', 'sec.external': 'Services externes',
|
||||
'sec.general': 'Général', 'sec.email': 'E-mail (SMTP)', 'sec.lookup': "Recherche d'indicatif",
|
||||
'sec.bands': 'Bandes', 'sec.modes': 'Modes & RST par défaut', 'sec.cluster': 'DX Cluster',
|
||||
'sec.udp': 'Intégrations UDP', 'sec.database': 'Base de données', 'sec.autostart': 'Démarrage auto', 'sec.backup': 'Sauvegarde base', 'station.title': 'Contrôle station', 'station.rotator': 'Rotator', 'station.rotatorNoRead': 'Azimut non lu', 'station.pattern': 'Diagramme', 'station.bi': 'Bi', 'station.retract': 'Rétracter les éléments', 'station.moving': 'EN MOUVEMENT', 'station.elements': 'Éléments (mm)', 'station.read': 'Lire', 'station.readLengths': 'Lire les longueurs actuelles depuis le contrôleur', 'station.noLengths': 'Longueurs inconnues — clique sur Lire pour les récupérer depuis le contrôleur.', 'station.element': 'Élément', 'station.reflector': 'Réflecteur', 'station.driven': 'Radiateur', 'station.director': 'Dir', 'station.set': 'Régler', 'station.elementsHint': "Chaque appui allonge/raccourcit l'élément de 2 mm (comme le pupitre). Vérifie quel élément répond sur ton antenne.", 'station.go': 'Aller', 'station.stop': 'Stop', 'station.dragHint': 'Glisse une carte pour réordonner. Choisis un nombre de colonnes pour la disposition.', 'station.colsAuto': 'Auto', 'station.addDevice': 'Ajouter un appareil', 'station.editDevice': "Modifier l'appareil", 'station.empty': "Aucune carte relais. Ajoute un WebSwitch 1216H ou une carte KMTronic 8 relais pour piloter l'alimentation et les accessoires de ta station.", 'station.online': 'En ligne', 'station.offline': 'Hors ligne', 'station.edit': 'Modifier', 'station.delete': 'Supprimer', 'station.relay': 'Relais', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': "Type d'appareil", 'station.name': 'Nom', 'station.host': 'Hôte / IP', 'station.user': "Nom d'utilisateur", 'station.pass': 'Mot de passe', 'station.optional': 'optionnel', 'station.labels': 'Libellés des relais', 'station.cancel': 'Annuler', 'station.save': 'Enregistrer',
|
||||
'sec.udp': 'Intégrations UDP', 'sec.database': 'Base de données', 'sec.autostart': 'Démarrage auto', 'sec.backup': 'Sauvegarde base', 'sec.uscounties': 'Comtés US',
|
||||
'uscty.title': 'Comtés US (USA-CA)',
|
||||
'uscty.intro': "Résout un indicatif US en comté et locator, hors-ligne, depuis la base de licences FCC ULS. Ça alimente le diplôme Comtés US et la chasse aux comtés — même en CW/SSB, où le spot ne porte qu'un indicatif.",
|
||||
'uscty.needDownload': "La résolution des comtés nécessite d'abord de télécharger la base FCC (environ 150 Mo, stockée en local). Rien n'est résolu tant que tu ne l'as pas téléchargée.",
|
||||
'uscty.dbStatus': 'Base des comtés',
|
||||
'uscty.loaded': '{n} indicatifs · maj {date}',
|
||||
'uscty.notLoaded': 'Pas encore téléchargée.',
|
||||
'uscty.download': 'Télécharger',
|
||||
'uscty.update': 'Mettre à jour',
|
||||
'uscty.done': 'Base des comtés prête — {n} indicatifs.',
|
||||
'uscty.backfillTitle': 'Compléter les QSO existants',
|
||||
'uscty.backfillIntro': "Résout le comté (et le locator) pour les QSO US déjà dans ton log qui n'en ont pas. Les valeurs existantes sont conservées — seuls les vides sont remplis.",
|
||||
'uscty.backfillRun': 'Remplir les comtés manquants',
|
||||
'uscty.backfillDone': '{s} QSO US analysés · {c} comtés, {g} locators remplis.',
|
||||
'station.title': 'Contrôle station', 'station.rotator': 'Rotator', 'station.rotatorNoRead': 'Azimut non lu', 'station.pattern': 'Diagramme', 'station.bi': 'Bi', 'station.retract': 'Rétracter les éléments', 'station.moving': 'EN MOUVEMENT', 'station.elements': 'Éléments (mm)', 'station.read': 'Lire', 'station.readLengths': 'Lire les longueurs actuelles depuis le contrôleur', 'station.noLengths': 'Longueurs inconnues — clique sur Lire pour les récupérer depuis le contrôleur.', 'station.element': 'Élément', 'station.reflector': 'Réflecteur', 'station.driven': 'Radiateur', 'station.director': 'Dir', 'station.set': 'Régler', 'station.elementsHint': "Chaque appui allonge/raccourcit l'élément de 2 mm (comme le pupitre). Vérifie quel élément répond sur ton antenne.", 'station.go': 'Aller', 'station.stop': 'Stop', 'station.dragHint': 'Glisse une carte pour réordonner. Choisis un nombre de colonnes pour la disposition.', 'station.colsAuto': 'Auto', 'station.addDevice': 'Ajouter un appareil', 'station.editDevice': "Modifier l'appareil", 'station.empty': "Aucune carte relais. Ajoute un WebSwitch 1216H ou une carte KMTronic 8 relais pour piloter l'alimentation et les accessoires de ta station.", 'station.online': 'En ligne', 'station.offline': 'Hors ligne', 'station.edit': 'Modifier', 'station.delete': 'Supprimer', 'station.relay': 'Relais', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': "Type d'appareil", 'station.name': 'Nom', 'station.host': 'Hôte / IP', 'station.user': "Nom d'utilisateur", 'station.pass': 'Mot de passe', 'station.optional': 'optionnel', 'station.labels': 'Libellés des relais', 'station.cancel': 'Annuler', 'station.save': 'Enregistrer',
|
||||
'sec.awards': 'Diplômes', 'sec.cat': 'Interface CAT', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'Manipulateur CW',
|
||||
'sec.antenna': 'Antenne motorisée', 'sec.antgenius': 'Antenna Genius', 'sec.pgxl': 'Power Genius', 'sec.flex': 'FlexRadio', 'sec.audio': 'Périphériques audio',
|
||||
'gen.hint': 'Comportement de l\'application (enregistré immédiatement).',
|
||||
|
||||
Reference in New Issue
Block a user