feat: implemented HRDLog upload

This commit is contained in:
2026-06-18 14:27:33 +02:00
parent e8eedcc1dc
commit cdd71b17c8
11 changed files with 333 additions and 8 deletions
@@ -23,6 +23,7 @@ type Confirmation = {
const SERVICES = [
{ v: 'qrz', label: 'QRZ.com' },
{ v: 'clublog', label: 'Club Log' },
{ v: 'hrdlog', label: 'HRDLog.net' },
{ v: 'lotw', label: 'LoTW' },
{ v: 'pota', label: 'POTA hunter log' },
{ v: 'paper', label: 'Paper QSL' },
+85 -5
View File
@@ -31,7 +31,7 @@ import {
GetTelemetryEnabled, SetTelemetryEnabled,
GetLiveStatusEnabled, SetLiveStatusEnabled,
GetQSLDefaults, SaveQSLDefaults,
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload,
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload, TestHRDLogUpload,
GetPOTAToken, SavePOTAToken,
TestLoTWUpload, ListTQSLStationLocations,
ComputeStationInfo,
@@ -747,20 +747,21 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
// wired today. upload_mode is 'immediate' | 'delayed' (per-service).
type ExtServiceCfg = {
api_key: string; email: string; username: string; password: string; callsign: string;
code: string;
force_station_callsign: string;
tqsl_path: string; station_location: string; key_password: string;
upload_flags: string[]; write_log: boolean;
auto_upload: boolean; upload_mode: string;
};
type ExternalServices = { qrz: ExtServiceCfg; clublog: ExtServiceCfg; lotw: ExtServiceCfg };
type ExternalServices = { qrz: ExtServiceCfg; clublog: ExtServiceCfg; lotw: ExtServiceCfg; hrdlog: ExtServiceCfg };
const emptyExtCfg = (): ExtServiceCfg => ({
api_key: '', email: '', username: '', password: '', callsign: '',
api_key: '', email: '', username: '', password: '', callsign: '', code: '',
force_station_callsign: '', tqsl_path: '', station_location: '', key_password: '',
upload_flags: ['N', 'R'], write_log: false,
auto_upload: false, upload_mode: 'immediate',
});
const [extSvc, setExtSvc] = useState<ExternalServices>({
qrz: emptyExtCfg(), clublog: emptyExtCfg(), lotw: emptyExtCfg(),
qrz: emptyExtCfg(), clublog: emptyExtCfg(), lotw: emptyExtCfg(), hrdlog: emptyExtCfg(),
});
const [qrzTest, setQrzTest] = useState<{ ok: boolean; msg: string } | null>(null);
const [qrzTesting, setQrzTesting] = useState(false);
@@ -768,6 +769,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
const [clublogTesting, setClublogTesting] = useState(false);
const [lotwTest, setLotwTest] = useState<{ ok: boolean; msg: string } | null>(null);
const [lotwTesting, setLotwTesting] = useState(false);
const [hrdlogTest, setHrdlogTest] = useState<{ ok: boolean; msg: string } | null>(null);
const [hrdlogTesting, setHrdlogTesting] = useState(false);
const [stationLocations, setStationLocations] = useState<string[]>([]);
// Active tab in the External Services panel — lifted here because
// PANELS[selected]() is called as a function, so panels can't hold hooks.
@@ -2603,7 +2606,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
const TABS: { k: typeof extSvcTab; label: string; ready?: boolean }[] = [
{ k: 'qrz', label: 'QRZ.COM', ready: true },
{ k: 'clublog', label: 'CLUBLOG', ready: true },
{ k: 'hrdlog', label: 'HRDLOG.NET' },
{ k: 'hrdlog', label: 'HRDLOG.NET', ready: true },
{ k: 'eqsl', label: 'EQSL' },
{ k: 'hamqth', label: 'HAMQTH' },
{ k: 'lotw', label: 'LOTW', ready: true },
@@ -2658,6 +2661,24 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
}
}
const hrdlog = extSvc.hrdlog;
const setHrdlog = (patch: Partial<ExtServiceCfg>) =>
setExtSvc((s) => ({ ...s, hrdlog: { ...s.hrdlog, ...patch } }));
async function testHrdlog() {
setHrdlogTesting(true);
setHrdlogTest(null);
try {
await SaveExternalServices(extSvc as any);
const msg = await TestHRDLogUpload();
setHrdlogTest({ ok: true, msg });
} catch (e: any) {
setHrdlogTest({ ok: false, msg: String(e?.message ?? e) });
} finally {
setHrdlogTesting(false);
}
}
const lotw = extSvc.lotw;
const setLotw = (patch: Partial<ExtServiceCfg>) =>
setExtSvc((s) => ({ ...s, lotw: { ...s.lotw, ...patch } }));
@@ -2833,6 +2854,65 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
</div>
</div>
</div>
) : extSvcTab === 'hrdlog' ? (
<div className="space-y-4 max-w-2xl">
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
<Label className="text-sm">Station callsign</Label>
<Input
value={hrdlog.callsign}
onChange={(e) => setHrdlog({ callsign: e.target.value.toUpperCase() })}
placeholder="defaults to the active profile's callsign"
className="font-mono text-xs"
/>
<Label className="text-sm">Upload code</Label>
<Input
type="password"
value={hrdlog.code}
onChange={(e) => setHrdlog({ code: e.target.value })}
placeholder="HRDLog account → My Account → Upload code"
className="text-xs"
/>
</div>
<div className="text-[10px] text-muted-foreground -mt-1">
Find the upload code on HRDLog.net under <em>My Account</em>. It authorises uploads for this callsign.
</div>
<div className="border-t border-border/60 pt-3 space-y-3">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox
checked={hrdlog.auto_upload}
onCheckedChange={(c) => setHrdlog({ auto_upload: !!c })}
/>
Automatic upload on new QSO
</label>
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
<Label className="text-sm">Upload timing</Label>
<Select
value={hrdlog.upload_mode || 'immediate'}
onValueChange={(v) => setHrdlog({ upload_mode: v })}
>
<SelectTrigger className="h-8 w-64"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="immediate">Immediate</SelectItem>
<SelectItem value="delayed">Delayed (12 min, lets you fix mistakes)</SelectItem>
<SelectItem value="on_close">On app close (batch)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" size="sm" onClick={testHrdlog} disabled={hrdlogTesting}>
<UploadCloud className="size-3.5" /> {hrdlogTesting ? 'Testing' : 'Test connection'}
</Button>
{hrdlogTest && (
<span className={cn('text-xs', hrdlogTest.ok ? 'text-emerald-700' : 'text-rose-700')}>
{hrdlogTest.msg}
</span>
)}
</div>
</div>
</div>
) : extSvcTab === 'lotw' ? (
<div className="space-y-4 max-w-2xl">
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">