chore: release v021.4
This commit is contained in:
@@ -3,7 +3,7 @@ import {
|
||||
ArrowDown, ArrowUp, ArrowLeft, ArrowRight, Copy, Plus, Star, StarOff, Trash2,
|
||||
ChevronDown, ChevronRight,
|
||||
User, Database, Radio, Cog, Server, Award, Antenna as AntennaIcon,
|
||||
Compass, Wifi, Construction, UploadCloud, Loader2, FolderOpen, Play, Power, Check, Eye, EyeOff, Pencil,
|
||||
Compass, Wifi, Construction, UploadCloud, Loader2, FolderOpen, Play, Power, Check, Pencil,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
GetLookupSettings, SaveLookupSettings, ClearLookupCache, TestLookupProvider,
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
GetAutostartPrograms, SaveAutostartPrograms, BrowseExecutable, LaunchAutostartProgram,
|
||||
GetTelemetryEnabled, SetTelemetryEnabled,
|
||||
GetQSLDefaults, SaveQSLDefaults,
|
||||
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload, TestHRDLogUpload, TestEQSLUpload,
|
||||
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload, TestHRDLogUpload, TestEQSLUpload, TestCloudlogUpload,
|
||||
GetPOTAToken, SavePOTAToken,
|
||||
TestLoTWUpload, ListTQSLStationLocations,
|
||||
DownloadLoTWUsers, GetLoTWUsersStatus,
|
||||
@@ -1178,7 +1178,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
from: '', reply_to: '', encryption: 'starttls', auth: true, auto_send: false, subject: '', body: '',
|
||||
});
|
||||
const [emailMsg, setEmailMsg] = useState('');
|
||||
const [showSmtpPass, setShowSmtpPass] = useState(false);
|
||||
const setEmailField = (patch: Partial<EmailCfg>) => setEmailCfg((s) => ({ ...s, ...patch }));
|
||||
// eQSL card e-mail (subject/body templates + auto-send on log).
|
||||
type EQSLCfg = { subject: string; body: string; auto_send: boolean };
|
||||
@@ -1223,20 +1222,21 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
type ExtServiceCfg = {
|
||||
api_key: string; email: string; username: string; password: string; callsign: string;
|
||||
code: string; qth_nickname: string;
|
||||
url: string; station_id: string; // Cloudlog/Wavelog: own instance + station profile
|
||||
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; hrdlog: ExtServiceCfg; eqsl: ExtServiceCfg };
|
||||
type ExternalServices = { qrz: ExtServiceCfg; clublog: ExtServiceCfg; lotw: ExtServiceCfg; hrdlog: ExtServiceCfg; eqsl: ExtServiceCfg; cloudlog: ExtServiceCfg };
|
||||
const emptyExtCfg = (): ExtServiceCfg => ({
|
||||
api_key: '', email: '', username: '', password: '', callsign: '', code: '', qth_nickname: '',
|
||||
api_key: '', url: '', station_id: '', email: '', username: '', password: '', callsign: '', code: '', qth_nickname: '',
|
||||
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(), hrdlog: emptyExtCfg(), eqsl: emptyExtCfg(),
|
||||
qrz: emptyExtCfg(), clublog: emptyExtCfg(), lotw: emptyExtCfg(), hrdlog: emptyExtCfg(), eqsl: emptyExtCfg(), cloudlog: emptyExtCfg(),
|
||||
});
|
||||
const [qrzTest, setQrzTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [qrzTesting, setQrzTesting] = useState(false);
|
||||
@@ -1299,12 +1299,14 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
};
|
||||
const [hrdlogTest, setHrdlogTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [hrdlogTesting, setHrdlogTesting] = useState(false);
|
||||
const [cloudlogTest, setCloudlogTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [cloudlogTesting, setCloudlogTesting] = useState(false);
|
||||
const [eqslTest, setEqslTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [eqslTesting, setEqslTesting] = 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.
|
||||
const [extSvcTab, setExtSvcTab] = useState<'qrz' | 'clublog' | 'hrdlog' | 'eqsl' | 'lotw' | 'pota'>('qrz');
|
||||
const [extSvcTab, setExtSvcTab] = useState<'qrz' | 'clublog' | 'hrdlog' | 'eqsl' | 'lotw' | 'cloudlog' | 'pota'>('qrz');
|
||||
// POTA hunter-log sync (stamps pota_ref on local QSOs from your pota.app log).
|
||||
const [potaToken, setPotaToken] = useState('');
|
||||
const [potaBusy, setPotaBusy] = useState(false);
|
||||
@@ -3756,6 +3758,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
{ k: 'hrdlog', label: 'HRDLOG.NET', ready: true },
|
||||
{ k: 'eqsl', label: 'EQSL', ready: true },
|
||||
{ k: 'lotw', label: 'LOTW', ready: true },
|
||||
{ k: 'cloudlog', label: 'CLOUDLOG', ready: true },
|
||||
{ k: 'pota', label: 'POTA', ready: true },
|
||||
];
|
||||
|
||||
@@ -3825,6 +3828,26 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
}
|
||||
}
|
||||
|
||||
const cloudlog = extSvc.cloudlog;
|
||||
const setCloudlog = (patch: Partial<ExtServiceCfg>) =>
|
||||
setExtSvc((s) => ({ ...s, cloudlog: { ...s.cloudlog, ...patch } }));
|
||||
|
||||
async function testCloudlog() {
|
||||
setCloudlogTesting(true);
|
||||
setCloudlogTest(null);
|
||||
try {
|
||||
// Save first: the backend test reads the stored config, so it must see
|
||||
// what was just typed (same as the other services' Test buttons).
|
||||
await SaveExternalServices(extSvc as any);
|
||||
const msg = await TestCloudlogUpload();
|
||||
setCloudlogTest({ ok: true, msg });
|
||||
} catch (e: any) {
|
||||
setCloudlogTest({ ok: false, msg: String(e?.message ?? e) });
|
||||
} finally {
|
||||
setCloudlogTesting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const eqsl = extSvc.eqsl;
|
||||
const setEqsl = (patch: Partial<ExtServiceCfg>) =>
|
||||
setExtSvc((s) => ({ ...s, eqsl: { ...s.eqsl, ...patch } }));
|
||||
@@ -4077,6 +4100,74 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : extSvcTab === 'cloudlog' ? (
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
|
||||
<Label className="text-sm">{t('es.cloudlogUrl')}</Label>
|
||||
<Input
|
||||
value={cloudlog.url}
|
||||
onChange={(e) => setCloudlog({ url: e.target.value })}
|
||||
placeholder={t('es.cloudlogUrlPh')}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<Label className="text-sm">{t('es.apiKey')}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={cloudlog.api_key}
|
||||
onChange={(e) => setCloudlog({ api_key: e.target.value })}
|
||||
placeholder={t('es.cloudlogKeyPh')}
|
||||
className="text-xs"
|
||||
/>
|
||||
<Label className="text-sm">{t('es.cloudlogStationId')}</Label>
|
||||
<Input
|
||||
value={cloudlog.station_id}
|
||||
onChange={(e) => setCloudlog({ station_id: e.target.value })}
|
||||
placeholder="1"
|
||||
className="font-mono text-xs w-24"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground -mt-1">
|
||||
{t('es.cloudlogHint')}
|
||||
</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={cloudlog.auto_upload}
|
||||
onCheckedChange={(c) => setCloudlog({ auto_upload: !!c })}
|
||||
/>
|
||||
{t('es.autoUpload')}
|
||||
</label>
|
||||
|
||||
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
|
||||
<Label className="text-sm">{t('es.uploadTiming')}</Label>
|
||||
<Select
|
||||
value={cloudlog.upload_mode === 'delayed' ? 'delayed' : 'immediate'}
|
||||
onValueChange={(v) => setCloudlog({ upload_mode: v })}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-64"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="immediate">{t('es.immediate')}</SelectItem>
|
||||
<SelectItem value="delayed">{t('es.delayed')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* No "on close" option: unlike the other services, Cloudlog has no
|
||||
per-QSO status column in the logbook, so there is nothing to
|
||||
select a backlog from at shutdown. */}
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" onClick={testCloudlog} disabled={cloudlogTesting}>
|
||||
<UploadCloud className="size-3.5" /> {cloudlogTesting ? t('es.testing') : t('es.testConn')}
|
||||
</Button>
|
||||
{cloudlogTest && (
|
||||
<span className={cn('text-xs', cloudlogTest.ok ? 'text-success' : 'text-danger')}>
|
||||
{cloudlogTest.msg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : extSvcTab === 'eqsl' ? (
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
|
||||
@@ -4992,15 +5083,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<Label className="text-sm">{t('em.username')}</Label>
|
||||
<Input className="h-8" disabled={!emailCfg.auth} value={emailCfg.smtp_user} onChange={(e) => setEmailField({ smtp_user: e.target.value })} />
|
||||
<Label className="text-sm">{t('es.password')}</Label>
|
||||
<div className="relative">
|
||||
<Input type={showSmtpPass ? 'text' : 'password'} className="h-8 pr-9" disabled={!emailCfg.auth} value={emailCfg.smtp_password} onChange={(e) => setEmailField({ smtp_password: e.target.value })} />
|
||||
<button type="button" tabIndex={-1} onClick={() => setShowSmtpPass((v) => !v)}
|
||||
title={showSmtpPass ? t('es.hidePass') : t('es.showPass')}
|
||||
className="absolute inset-y-0 right-0 flex items-center px-2.5 text-muted-foreground hover:text-foreground disabled:opacity-40"
|
||||
disabled={!emailCfg.auth}>
|
||||
{showSmtpPass ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</button>
|
||||
</div>
|
||||
{/* No reveal button: the settings window is often open while the
|
||||
screen is shared or shown to someone in the shack. */}
|
||||
<Input type="password" className="h-8" disabled={!emailCfg.auth} value={emailCfg.smtp_password} onChange={(e) => setEmailField({ smtp_password: e.target.value })} />
|
||||
<Label className="text-sm">{t('em.fromAddr')}</Label>
|
||||
<Input className="h-8" placeholder="[email protected]" value={emailCfg.from} onChange={(e) => setEmailField({ from: e.target.value })} />
|
||||
<Label className="text-sm">{t('em.replyTo')}</Label>
|
||||
|
||||
Reference in New Issue
Block a user