feat: implemented HRDLog upload
This commit is contained in:
@@ -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' },
|
||||
|
||||
@@ -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 (1–2 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">
|
||||
|
||||
Reference in New Issue
Block a user