chore: release v0.20.7

This commit is contained in:
2026-07-21 18:26:34 +02:00
parent 828f99b8ac
commit 24a5a0480d
15 changed files with 909 additions and 183 deletions
+106 -34
View File
@@ -13,7 +13,7 @@ import {
GetRotatorSettings, SaveRotatorSettings, TestRotator, RotatorPark, RotatorStop,
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam,
GetAntGeniusSettings, SaveAntGeniusSettings,
GetPGXLSettings, SavePGXLSettings, GetSPEStatus, SPESetOperate,
GetPGXLSettings, SavePGXLSettings, GetSPEStatus, SPESetOperate, GetACOMStatus, ACOMSetOperate,
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts,
GetAudioSettings, SaveAudioSettings, ListAudioInputDevices, ListAudioOutputDevices, PickAudioFolder, TestPTT,
GetClublogCtyInfo, SetClublogCtyEnabled, DownloadClublogCty,
@@ -32,7 +32,6 @@ import {
GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection, GetDBBackendStatus,
GetAutostartPrograms, SaveAutostartPrograms, BrowseExecutable, LaunchAutostartProgram,
GetTelemetryEnabled, SetTelemetryEnabled,
GetLiveStatusEnabled, SetLiveStatusEnabled,
GetQSLDefaults, SaveQSLDefaults,
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload, TestHRDLogUpload, TestEQSLUpload,
GetPOTAToken, SavePOTAToken,
@@ -566,26 +565,6 @@ function TelemetryToggle() {
);
}
// LiveStatusToggle publishes this operator's current activity (call + band +
// freq + mode) to the shared MySQL `live_status` table every ~15s, for multi-op
// events — a small web script on your server renders it for the QRZ page. Only
// useful on a MySQL logbook. Self-contained component (owns its async state).
function LiveStatusToggle() {
const { t } = useI18n();
const [on, setOn] = useState(false);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
GetLiveStatusEnabled().then((v) => setOn(!!v)).catch(() => {}).finally(() => setLoaded(true));
}, []);
return (
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={on} disabled={!loaded}
onCheckedChange={(c) => { const v = !!c; setOn(v); SetLiveStatusEnabled(v).catch(() => {}); }} />
{t('settings.liveStatus')} <span className="text-xs text-muted-foreground">({t('settings.liveStatusHint')})</span>
</label>
);
}
// ADIFMonitorPanel watches a list of external ADIF files (fldigi RTTY, N1MM,
// VarAC…) and auto-imports newly appended QSOs — deliberately option-free: a
// contact that arrives is imported and uploaded automatically like any log entry.
@@ -691,6 +670,47 @@ function SPEStatusCard() {
</div>
);
}
// Live ACOM amplifier status + OPERATE/STANDBY. Module-scoped for the same
// remount reason as SPEStatusCard. Polls once a second while shown.
function ACOMStatusCard() {
const [st, setSt] = useState<any>({ connected: false });
useEffect(() => {
let alive = true;
const tick = () => GetACOMStatus().then((s) => alive && setSt(s || {})).catch(() => {});
tick();
const id = window.setInterval(tick, 1000);
return () => { alive = false; window.clearInterval(id); };
}, []);
const operate = !!st.operate;
return (
<div className="rounded-md border border-border p-3 space-y-2 text-xs max-w-xl">
<div className="flex items-center gap-2">
<span className={cn('size-2 rounded-full', st.connected ? 'bg-success animate-pulse' : 'bg-danger')} />
<span className="font-semibold">{st.connected ? `ACOM ${st.model || ''}` : `ACOM ${st.model || ''} — not connected`}</span>
{!st.connected && st.last_error && <span className="text-danger truncate text-[10px]">{st.last_error}</span>}
<span className="flex-1" />
<Button size="sm" variant={operate ? 'default' : 'outline'} disabled={!st.connected}
onClick={() => ACOMSetOperate(!operate).catch(() => {})}
title="Toggle OPERATE / STANDBY">
{operate ? 'OPERATE' : 'STANDBY'}
</Button>
</div>
{st.connected && (
<div className="grid grid-cols-4 gap-x-3 gap-y-1 font-mono text-[11px]">
<div>{st.state}</div>
<div>Band {st.band || '—'}</div>
<div>{st.fwd_w} W</div>
<div>SWR {Number(st.swr ?? 0).toFixed(2)}</div>
<div>Refl {st.refl_w} W</div>
<div>Drive {st.drive_w} W</div>
<div>{st.temp_c}°C</div>
<div>Fan {st.fan}</div>
{st.err_text && <div className="col-span-4 text-warning"> {st.err_text} ({st.err_code})</div>}
</div>
)}
</div>
);
}
function RelayAutoPanel() {
const { t } = useI18n();
const [enabled, setEnabled] = useState(false);
@@ -2690,7 +2710,32 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
function PGXLPanelSettings() {
const isPGXL = pgxl.type === 'pgxl';
const isACOM = (pgxl.type || '').startsWith('acom');
const isSerial = pgxl.transport === 'serial';
// The stored `type` stays the single flat value ("spe13", "acom700", "pgxl" —
// binding compatibility); the UI presents it as brand + model.
const brand = isPGXL ? 'pgxl' : isACOM ? 'acom' : 'spe';
const brandModels: Record<string, { value: string; label: string }[]> = {
spe: [
{ value: 'spe13', label: 'Expert 1.3K-FA' },
{ value: 'spe15', label: 'Expert 1.5K-FA' },
{ value: 'spe2k', label: 'Expert 2K-FA' },
],
acom: [
{ value: 'acom500', label: '500S' },
{ value: 'acom600', label: '600S' },
{ value: 'acom700', label: '700S' },
{ value: 'acom1200', label: '1200S' },
{ value: 'acom2020', label: '2020S' },
],
};
// Each family has a fixed serial speed: SPE talks 115200, the ACOM S-series is
// 9600 8N1 — preset it so switching brand just works. PGXL is TCP-only.
const applyType = (v: string) => setPgxl((s) => ({
...s, type: v,
transport: v === 'pgxl' ? 'tcp' : s.transport,
baud: v.startsWith('acom') ? 9600 : v.startsWith('spe') ? 115200 : s.baud,
}));
return (
<>
<SectionHeader title="Amplifier" />
@@ -2700,21 +2745,44 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
Enable amplifier control
</label>
<div className="grid grid-cols-2 gap-3">
{/* Connection gets the widest column — "Network (RS232-to-Ethernet)" was
truncated with 3 equal columns. */}
<div className="grid grid-cols-[1fr_1fr_1.7fr] gap-3">
<div className="space-y-1">
<Label>Amplifier</Label>
<Select value={pgxl.type} onValueChange={(v) => setPgxl((s) => ({ ...s, type: v, transport: v === 'pgxl' ? 'tcp' : s.transport }))}>
<Label>Brand</Label>
<Select value={brand} onValueChange={(b) => {
if (b === brand) return;
applyType(b === 'pgxl' ? 'pgxl' : brandModels[b][0].value);
}}>
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="pgxl">4O3A PowerGenius XL</SelectItem>
<SelectItem value="spe13">SPE Expert 1.3K-FA</SelectItem>
<SelectItem value="spe15">SPE Expert 1.5K-FA</SelectItem>
<SelectItem value="spe2k">SPE Expert 2K-FA</SelectItem>
<SelectItem value="pgxl">4O3A</SelectItem>
<SelectItem value="spe">SPE</SelectItem>
<SelectItem value="acom">ACOM</SelectItem>
</SelectContent>
</Select>
</div>
{/* PowerGenius is TCP-only; SPE amps connect over USB serial or an
RS232-to-Ethernet bridge, so they offer both. */}
<div className="space-y-1">
<Label>Model</Label>
{isPGXL ? (
// The PowerGenius is the brand's single model — fixed, no choice.
<Select value="pgxl" disabled>
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="pgxl">PowerGenius XL</SelectItem>
</SelectContent>
</Select>
) : (
<Select value={pgxl.type} onValueChange={applyType}>
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
<SelectContent>
{brandModels[brand].map((m) => <SelectItem key={m.value} value={m.value}>{m.label}</SelectItem>)}
</SelectContent>
</Select>
)}
</div>
{/* PowerGenius is TCP-only; SPE and ACOM amps connect over USB serial or
an RS232-to-Ethernet bridge, so they offer both. */}
{!isPGXL && (
<div className="space-y-1">
<Label>Connection</Label>
@@ -2767,12 +2835,17 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
</div>
)}
{!isPGXL && pgxl.enabled && <SPEStatusCard />}
{!isPGXL && (
{!isPGXL && pgxl.enabled && (isACOM ? <ACOMStatusCard /> : <SPEStatusCard />)}
{!isPGXL && !isACOM && (
<p className="text-[10px] text-muted-foreground">
SPE control uses the amplifier's proprietary serial protocol (OPERATE toggle + live status). Save to (re)connect. Band / power-level / antenna are still managed on the amp from the transceiver CAT.
</p>
)}
{isACOM && (
<p className="text-[10px] text-muted-foreground">
ACOM control uses the amplifier's serial protocol (9600 8N1): OPERATE / STANDBY / OFF + live telemetry. Save to (re)connect. Power-ON needs the serial DTR/RTS pins wired in the cable (not available over a network bridge). Band tracking stays on the amp itself.
</p>
)}
</div>
</>
);
@@ -4515,7 +4588,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
{t('gen.checkUpdates')} <span className="text-xs text-muted-foreground">{t('gen.checkUpdatesHint')}</span>
</label>
<TelemetryToggle />
<LiveStatusToggle />
<MainViewPanes onChanged={onMainPaneChanged} flexAvailable={flexAvailable} icomAvailable={icomAvailable} />