chore: release v0.21.3

This commit is contained in:
2026-07-26 16:57:19 +02:00
parent 4fd70f6a9d
commit 91b5af1c7b
46 changed files with 2491 additions and 355 deletions
+41 -5
View File
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useRef, useState } from 'react';
import { Gauge, Radio, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { MeterBar } from '@/components/MeterBar';
@@ -66,9 +66,42 @@ function ChannelButton({ letter, ch, active, ptt, threeWay, onSelect, t }: {
}
export function TunerCard({ status, t }: { status: TGStatus; t: (k: string, v?: any) => string }) {
// Peak-hold, exactly as AmpCard does it. Both cards read the same transmitter,
// but the tuner is sampled by a 400 ms TCP poll: on SSB that lands in the gaps
// between syllables as often as on a peak, so the raw reading collapses to zero
// several times a second mid-transmission and reads as a dropped link. The
// amplifier looked steady next to it only because it already smoothed this way.
// Hold the highest value seen, decaying after 2 s so it follows a real drop.
const peak = useRef<Record<string, { v: number; t: number }>>({});
const peakHold = (key: string, val: number) => {
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; }
return p.v;
};
const connected = !!status.connected;
const vswr = status.vswr && status.vswr > 0 ? status.vswr : undefined;
const fwdW = status.fwd_w && status.fwd_w >= 1 ? status.fwd_w : 0;
const rawVswr = status.vswr && status.vswr > 0 ? status.vswr : undefined;
const rawFwdW = status.fwd_w && status.fwd_w >= 1 ? status.fwd_w : 0;
const fwdW = peakHold('fwd', rawFwdW);
// SWR needs RF to mean anything: sampled on receive the device reports a
// meaningless 1.00, which would wipe the figure the operator actually wants —
// the one measured while transmitting. So show the live value during TX, keep it
// briefly afterwards so it can be read, then let it go.
//
// That expiry is the point: the first version of this held the last TX value
// with no timeout at all, so the meter sat frozen on the previous transmission
// indefinitely — reading 1.39:1 with the radio plainly in RX and 0 W forward.
// Same 2 s window as the power peak above, so the two meters clear together.
const swrHold = useRef<{ v: number; t: number } | null>(null);
if (rawFwdW >= 1 && rawVswr) swrHold.current = { v: rawVswr, t: Date.now() };
const heldSwr = swrHold.current;
const vswr = rawFwdW >= 1
? rawVswr
: heldSwr && Date.now() - heldSwr.t < 2000
? heldSwr.v
: undefined;
const active = status.active ?? 1;
const a: TGChannel = status.a ?? {};
const b: TGChannel = status.b ?? {};
@@ -115,8 +148,11 @@ export function TunerCard({ status, t }: { status: TGStatus; t: (k: string, v?:
onSelect={() => TunerGeniusActivate(2).catch(() => {})} t={t} />
</div>
{/* PWR + SWR meters — same grid as the Flex/amp meters so they match size. */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{/* PWR + SWR meters, each taking half the card. The amp card's grid is
3-wide because it shows three meters; copying it here left the tuner's
two meters in the first two of three columns, with a third of the card
empty to the right of SWR. */}
<div className="grid grid-cols-2 gap-2">
<MeterBar label={t('tgp.power')} value={fwdW} unit="W" lo={0} hi={2000}
display={fwdW >= 1 ? `${Math.round(fwdW)} W` : '—'}
segColor={(f) => (f > 0.9 ? '#dc2626' : f > 0.75 ? '#f59e0b' : '#ea580c')} />