chore: release v0.22.9

This commit is contained in:
2026-08-02 06:40:10 +02:00
parent 296a4a55c0
commit f6cd6e999a
38 changed files with 1308 additions and 297 deletions
+17 -17
View File
@@ -86,13 +86,6 @@ function Slider({ value, onChange, disabled, accent = '#16a34a', step = 1, max =
if (!el) return;
const onWheel = (e: WheelEvent) => {
if (disRef.current) return;
// The wheel only acts on a slider the operator has DELIBERATELY taken
// hold of — one that has focus. Hovering was enough before, so scrolling
// the panel with the pointer anywhere over these controls silently moved
// whatever sat under it: an IC-7800 owner watched his transmit power
// change on its own. Without focus the event is left alone and the panel
// scrolls, which is what the gesture was for.
if (document.activeElement !== el) return;
e.preventDefault();
const d = e.deltaY < 0 ? stepRef.current : -stepRef.current;
const nv = Math.max(0, Math.min(maxRef.current, valRef.current + d));
@@ -330,13 +323,15 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [st.cw_speed]);
// Peak-hold: keep the highest reading for ~2 s so the jittery VITA-49 meters
// read steadily instead of jumping every poll.
// Peak-hold: keep the highest reading for a second so the jittery VITA-49
// meters read steadily instead of jumping every poll. One second, not two —
// longer and the meters visibly trail the end of a transmission, which reads
// as the app lagging rather than as a peak being held.
const peak = useRef<Record<string, { v: number; t: number }>>({});
const peakHold = (key: string, val: number) => {
const peakHold = (key: string, val: number, windowMs = 1000) => {
const now = Date.now();
const p = peak.current[key];
if (!p || val >= p.v || now - p.t > 2000) { peak.current[key] = { v: val, t: now }; return val; }
if (!p || val >= p.v || now - p.t > windowMs) { peak.current[key] = { v: val, t: now }; return val; }
return p.v;
};
@@ -362,7 +357,7 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
}, []);
// PowerGenius XL direct connection (fan mode), independent of the Flex link.
const [pg, setPg] = useState<{ connected: boolean; fan_mode?: string; host?: string; last_error?: string }>({ connected: false });
const [pg, setPg] = useState<{ connected: boolean; fan_mode?: string; host?: string; last_error?: string; state?: string; peak_w?: number }>({ connected: false });
useEffect(() => {
let alive = true;
const tick = async () => { try { const s: any = await GetPGXLStatus(); if (alive) setPg(s || { connected: false }); } catch {} };
@@ -985,10 +980,12 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
spe.operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
{spe.operate ? 'OPERATE' : 'STANDBY'}
</button>
{/* Power ON / OFF (best-guess keystrokes from the APG — verify on hw). */}
{/* Power ON pulses RTS then DTR — it must stay clickable while the amp
is off (no status = not "connected"), and serial only. */}
<div className="inline-flex rounded-lg overflow-hidden border-2 border-success/70">
<button type="button" disabled={!spe.connected}
<button type="button" disabled={!(spe.connected || spe.transport === 'serial')}
onClick={() => AmpPower(selAmp.id, true).catch(() => {})}
title={spe.transport === 'serial' ? 'Power on (RTS then DTR pulse)' : 'Power-on needs the serial RTS/DTR lines — not available over a network bridge'}
className="px-3 py-2 text-sm font-bold bg-card text-success hover:bg-success/15 disabled:opacity-30">ON</button>
<button type="button" disabled={!spe.connected}
onClick={() => AmpPower(selAmp.id, false).catch(() => {})}
@@ -1090,9 +1087,6 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
st.amp_operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
{st.amp_operate ? 'OPERATE' : 'STANDBY'}
</button>
<span className="text-xs text-muted-foreground">
{st.amp_operate ? t('flxp.ampInLine') : t('flxp.ampBypassed')}
</span>
{/* Fan mode — shown when the PowerGenius is configured (Settings →
PowerGenius). The dot shows the direct-connection state; the
selector is disabled until connected (hover it for the error). */}
@@ -1125,6 +1119,12 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
const amp = meters.filter((m) => (m.src || '').toUpperCase().includes('AMP')
&& !/^(RL|DRV)$/i.test((m.name || '').trim()));
if (off || amp.length === 0) return null;
// Power comes from the radio's meter stream and nothing else. The
// amplifier also reports a "peakfwd", and using it was a mistake
// twice over: it is a latched maximum that is never reset, and it
// survives in the last-known status after the amp disconnects — so
// it claimed 1350 W from an old transmission while the radio was
// putting out 10.
return (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2 pt-2 border-t border-border/50">
{amp.map((m) => {