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,
|
||||
|
||||
Reference in New Issue
Block a user