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:
+38
-3
@@ -119,6 +119,8 @@ import { DetailsPanel, type DetailsState } from '@/components/DetailsPanel';
|
||||
import { SendSpotModal, type RecentSpotQSO } from '@/components/SendSpotModal';
|
||||
import { WinkeyerPanel, type WKStatus, type WKMacro } from '@/components/WinkeyerPanel';
|
||||
import { RotorCompass } from '@/components/RotorCompass';
|
||||
import { RotorCompassClassic } from '@/components/RotorCompassClassic';
|
||||
import { rotorStyle, subscribeRotorStyle } from '@/lib/rotorStyle';
|
||||
import { GridSquareMap } from '@/components/GridSquareMap';
|
||||
import { loadClusterMacros, visibleClusterMacros } from '@/lib/clusterMacros';
|
||||
import { DecodesPanel, type Decode as DecodeRow, type TxMsg as TxMsgRow } from '@/components/DecodesPanel';
|
||||
@@ -2612,6 +2614,30 @@ export default function App() {
|
||||
// The band each receiver was last decoding on. A band change empties that
|
||||
// receiver's list — see the flush below.
|
||||
const decoderBandRef = useRef<Map<string, string>>(new Map());
|
||||
|
||||
// The RIG changing band empties the list too, without waiting for the
|
||||
// decoder to catch up.
|
||||
//
|
||||
// Clicking a watchlist row or a spot tunes the radio; the decoder follows on
|
||||
// its own clock and its first decode on the new band is up to a period away.
|
||||
// Until then the operator reads a screen of stations from a band they have
|
||||
// left — which is the moment the list is most misleading, because it looks
|
||||
// current. Only the receivers that were on the band we came FROM are
|
||||
// cleared: a second decoder parked on another band has not moved.
|
||||
const rigBandRef = useRef<string>('');
|
||||
useEffect(() => {
|
||||
const band = (catState.band ?? '').toLowerCase();
|
||||
const was = rigBandRef.current;
|
||||
rigBandRef.current = band;
|
||||
if (!band || !was || was === band) return;
|
||||
const stale = new Set<string>();
|
||||
decoderBandRef.current.forEach((b2, inst) => { if (b2 === was) stale.add(inst); });
|
||||
if (stale.size === 0) return;
|
||||
stale.forEach((inst) => decoderBandRef.current.delete(inst));
|
||||
pendingDecodesRef.current = pendingDecodesRef.current.filter((d) => !stale.has(d.instance ?? ''));
|
||||
setDecodes((arr) => arr.filter((d) => !stale.has(d.instance ?? '')));
|
||||
setTxMsgs((arr) => arr.filter((m) => !stale.has(m.instance ?? '')));
|
||||
}, [catState.band]);
|
||||
const pendingDecodeTimer = useRef<number | undefined>(undefined);
|
||||
|
||||
// Rotor quick-turn buttons (Settings → Rotator). Same reload trigger as the
|
||||
@@ -2774,6 +2800,11 @@ export default function App() {
|
||||
// withholding them IS the compact mode, so there is no second layout to keep
|
||||
// in step with the first.
|
||||
const [rotorCompact, setRotorCompact] = useState(() => localStorage.getItem('opslog.rotorCompact') === '1');
|
||||
// Which dial (Settings → Rotator). Subscribed rather than re-read on close, so
|
||||
// the choice shows the moment it is made — it is picked by looking at it.
|
||||
const [rotorDial, setRotorDial] = useState(() => rotorStyle());
|
||||
useEffect(() => subscribeRotorStyle(() => setRotorDial(rotorStyle())), []);
|
||||
const Compass2 = rotorDial === 'classic' ? RotorCompassClassic : RotorCompass;
|
||||
|
||||
// Award code → scanned field (e.g. POTA→pota_ref, WWFF→wwff). Used to route
|
||||
// picked award references to the QSO field/extras each award actually reads.
|
||||
@@ -6373,6 +6404,7 @@ export default function App() {
|
||||
// compare with", never "the rig is on no band".
|
||||
rigBand={catState.connected ? (catState.band || '') : ''}
|
||||
myCall={station.callsign}
|
||||
myGrid={station.my_grid}
|
||||
// A DOUBLE click answers the station: it hands the decode back to
|
||||
// WSJT-X/MSHV as a Reply, the same thing as double-clicking the line in
|
||||
// their own window. A single click only selects it — see onSelect below,
|
||||
@@ -7558,10 +7590,13 @@ export default function App() {
|
||||
{/* Rotor compass: azimuth dial + needles + click-to-turn. Shows when a
|
||||
rotator is configured or a DX bearing exists. Compact mode drops the
|
||||
controls column, so the widget is just the dial and needs only its
|
||||
width. */}
|
||||
width. The classic dial sizes itself from the inside and expects a fixed
|
||||
column; the current one asks for the width it needs. */}
|
||||
{showRotor && (rotatorHeading.enabled || dxPath) && (
|
||||
<div className={cn('shrink-0 min-h-0', rotorCompact ? 'w-[196px]' : 'w-[320px]')} style={{ order: wOrder('rotor') }}>
|
||||
<RotorCompass
|
||||
<div className={cn('shrink-0 min-h-0',
|
||||
rotorCompact ? 'w-[196px]' : rotorDial === 'classic' ? 'w-[320px]' : 'w-auto')}
|
||||
style={{ order: wOrder('rotor') }}>
|
||||
<Compass2
|
||||
presets={rotorCompact ? undefined : rotorPresets}
|
||||
onStop={rotorCompact ? undefined : () => { RotatorStop().then(pokeRotorHeading).catch((err) => setError(String(err?.message ?? err))); }}
|
||||
bearing={dxPath?.bearingShort ?? null}
|
||||
|
||||
Reference in New Issue
Block a user