fix(autocall): the QSO in progress outranks the ladder

Six faults from an evening on 60 m, all in the same family: the engine
judging a station by what the log wants from it and forgetting what is
already under way.

- An exchange was abandoned mid-QSO. The reply lands in the same period
  the ladder is re-read, and that period was judged before the reply was
  taken into account, so a better-ranked caller took the slot from a
  station that had just come back to us. The answer is settled first now,
  and our own report counts as being inside the exchange too — which also
  protects a QSO the operator started by hand.
- A station just picked started with misses against it. Its transmit slot
  was unknown until a second decode, and with the parity unknown every
  period counted, including the one spent transmitting to it.
- The freed slot after "it is working somebody else" was thrown away: the
  period's decodes are in hand, so the next station is picked from them
  rather than fifteen seconds later. Never mid-over.
- Auto-call is never armed from a stored setting — not at launch, not on
  a profile switch. It is the one feature that puts the station on the
  air by itself and OpsLog starts with Windows.
- It says what it is waiting for: a wanted station in a QSO with somebody
  else now shows beside the Auto button instead of looking idle.
- Switching profile left the previous logbook's verdicts on screen. The
  worked-index, chase-new and the frontend's cached verdicts are dropped
  when the logbook changes.

FT decodes: distance column, a message addressed to you set whole in
green (the station you are calling keeps a tint — most of what it sends
goes to other people), badge order L / Wkd / WL, list cleared when the
RIG changes band.

Rotor: new world-map compass from EC1KD's design, with the Ultrabeam boom
and second lobe restored and the compact form preserved; the classic dial
is kept and Settings → Rotator chooses between them. Stop no longer
flickers on a rotor standing still — movement was inferred from a degree,
less than the jitter a controller reports at rest.
This commit is contained in:
2026-09-05 23:02:31 +02:00
parent 8dbc4b7e62
commit 23323c91e0
14 changed files with 1752 additions and 389 deletions
@@ -11,6 +11,8 @@ import { useI18n } from '@/lib/i18n';
import { writeUiPref } from '@/lib/uiPref';
import { subscribeRotorHeading, pokeRotorHeading } from '@/lib/rotorHeading';
import { RotorCompass } from '@/components/RotorCompass';
import { RotorCompassClassic } from '@/components/RotorCompassClassic';
import { rotorStyle, subscribeRotorStyle } from '@/lib/rotorStyle';
import { AmpCard } from '@/components/AmpCard';
import { TunerCard } from '@/components/TunerCard';
import type { TGStatus } from '@/components/TunerGeniusPanel';
@@ -128,6 +130,11 @@ function RotatorWidget({ hd, refetch, centerLat, centerLon, bearing, t }: Rotato
}) {
const [goto, setGoto] = useState('');
const [err, setErr] = useState('');
// Both compasses in the app follow the same preference (Settings → Rotator):
// one operator's choice of dial, not one per panel.
const [dial, setDial] = useState(() => rotorStyle());
useEffect(() => subscribeRotorStyle(() => setDial(rotorStyle())), []);
const Dial = dial === 'classic' ? RotorCompassClassic : RotorCompass;
const turn = (az: number) => {
const a = ((Math.round(az) % 360) + 360) % 360;
@@ -145,9 +152,12 @@ function RotatorWidget({ hd, refetch, centerLat, centerLon, bearing, t }: Rotato
title={hd.ok ? t('station.online') : t('station.rotatorNoRead')} />
</div>
<div className="p-3 flex gap-4 items-start">
{/* The SP/LP readout lives INSIDE RotorCompass, so every compass in the
app carries it rather than each caller drawing its own. */}
<RotorCompass
{/* The compact compass is the dial alone — short and long path are the
two coloured dots on it. The figures are in the status bar. */}
{/* The dial-only form is square and fills the box it is given, so its
size is set here rather than baked into the component. */}
<div className={dial === 'classic' ? 'shrink-0' : 'w-[210px] shrink-0'}>
<Dial
bearing={bearing ?? null}
headings={hd.ok ? [hd.azimuth] : []}
centerLat={centerLat ?? null}
@@ -158,6 +168,7 @@ function RotatorWidget({ hd, refetch, centerLat, centerLon, bearing, t }: Rotato
onSelectRotor={(i) => { SetActiveRotor(i).then(refetch).catch((e) => setErr(String(e?.message ?? e))); }}
onGoto={(az) => turn(az)}
/>
</div>
<div className="flex-1 min-w-0 space-y-2">
<div className="font-mono">
<span className="text-2xl font-bold tabular-nums">{hd.ok ? `${hd.azimuth}°` : '—'}</span>