up
This commit is contained in:
+124
-73
@@ -242,10 +242,10 @@ export default function App() {
|
||||
setLocks((s) => {
|
||||
const wasLocked = s[k];
|
||||
const next = { ...s, [k]: !wasLocked };
|
||||
// Unlocking → restore automatic behavior. Without this the locked
|
||||
// value would linger forever: a stale Start time would never refresh
|
||||
// even after a new callsign is entered.
|
||||
if (wasLocked) {
|
||||
// Unlocking → restore automatic behavior. Without this the locked
|
||||
// value would linger forever: a stale Start time would never refresh
|
||||
// even after a new callsign is entered.
|
||||
if (k === 'start') {
|
||||
// If a QSO is currently in progress (callsign typed), snap start
|
||||
// to now since we missed the auto-start moment. Otherwise clear.
|
||||
@@ -254,6 +254,12 @@ export default function App() {
|
||||
// Drop the frozen end so the field tracks the live UTC clock.
|
||||
setQsoEndedAt(null);
|
||||
}
|
||||
} else {
|
||||
// Locking (manual / deferred entry) → pre-fill with today's date + the
|
||||
// current UTC time so the fields aren't empty; the operator just adjusts.
|
||||
const now = new Date();
|
||||
if (k === 'start') setQsoStartedAt((d) => d ?? now);
|
||||
else if (k === 'end') setQsoEndedAt((d) => d ?? now);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
@@ -857,6 +863,24 @@ export default function App() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// tuneRigCAT sends freq + mode to the rig, sequenced with a short settle
|
||||
// delay so an older transceiver — busy after a band/freq change — doesn't drop
|
||||
// the second command (which forced a second click to apply the mode). The
|
||||
// "mode before frequency" order (à la Log4OM) is an option for rigs that need
|
||||
// the mode set first; default is frequency-then-mode.
|
||||
async function tuneRigCAT(freqHz: number, mode: string) {
|
||||
const modeFirst = localStorage.getItem('opslog.catModeBeforeFreq') === '1';
|
||||
const doFreq = () => SetCATFrequency(freqHz).catch(() => {});
|
||||
const doMode = () => (mode ? SetCATMode(mode).catch(() => {}) : Promise.resolve());
|
||||
const settle = () => new Promise((r) => window.setTimeout(r, 150));
|
||||
if (modeFirst) {
|
||||
await doMode(); await settle(); await doFreq();
|
||||
} else {
|
||||
await doFreq();
|
||||
if (mode) { await settle(); await doMode(); }
|
||||
}
|
||||
}
|
||||
|
||||
// applyModeFromSpot updates the mode AND its RST default for a fresh target
|
||||
// (clicked spot / rig-driven mode change). Unlike a manual mode tweak, this
|
||||
// is a new contact, so we clear the "user edited RST" flag first — otherwise
|
||||
@@ -889,55 +913,65 @@ export default function App() {
|
||||
loadStation();
|
||||
loadLists();
|
||||
loadCATCfg();
|
||||
// Hydrate CAT state on mount (the backend may already be polling).
|
||||
try { setCatState(await GetCATState()); } catch {}
|
||||
})();
|
||||
// Poll the CAT state at launch until the rig reports a frequency: the
|
||||
// backend connects asynchronously and only PUSHES cat:state on change, so
|
||||
// the first (freq-carrying) event can fire before this listener mounts and
|
||||
// be missed — leaving the display blank until the operator next moves the
|
||||
// VFO. Stops once a freq arrives, CAT is off, or after ~13s.
|
||||
(async () => {
|
||||
for (let i = 0; i < 16; i++) {
|
||||
try {
|
||||
const s = await GetCATState();
|
||||
applyCatState(s);
|
||||
if (!s?.enabled) break;
|
||||
if (s?.connected && s.freq_hz && s.freq_hz > 0) break;
|
||||
} catch { /* not ready yet */ }
|
||||
await new Promise((r) => window.setTimeout(r, 800));
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Apply a CAT snapshot to the entry strip (freq/band/mode), unless the user
|
||||
// just typed something (freeze window) or locked a field. Shared by the live
|
||||
// cat:state event and the startup poll below.
|
||||
function applyCatState(s: CATState) {
|
||||
setCatState(s);
|
||||
if (!s?.connected) return;
|
||||
if (Date.now() < catFreezeUntilRef.current) return;
|
||||
const lk = locksRef.current;
|
||||
if (!lk.freq && s.freq_hz && s.freq_hz > 0) {
|
||||
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
||||
}
|
||||
// RX freq: backend follows ADIF — freq_hz = TX, freq_rx_hz = RX.
|
||||
if (!lk.freq) {
|
||||
if (s.split && s.freq_rx_hz && s.freq_rx_hz > 0) {
|
||||
setRxFreqMhz((s.freq_rx_hz / 1_000_000).toFixed(5));
|
||||
} else if (s.freq_hz && s.freq_hz > 0) {
|
||||
setRxFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
||||
}
|
||||
}
|
||||
if (!lk.band && s.band) setBand(s.band);
|
||||
|
||||
// Mode resolution priority: digital watering-hole → CAT's DATA → CAT mode.
|
||||
if (!lk.mode) {
|
||||
const inferred = s.freq_hz ? inferDigitalMode(s.freq_hz) : '';
|
||||
let nextMode = '';
|
||||
if (inferred) nextMode = inferred;
|
||||
else if (s.mode === 'DATA') nextMode = digitalDefaultRef.current || 'FT8';
|
||||
else if (s.mode) nextMode = s.mode;
|
||||
if (nextMode) {
|
||||
setMode(nextMode);
|
||||
applyModePreset(nextMode); // flip 599↔59 on mode change (respects edits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CAT live updates. Push freq/band/mode into the entry strip when the rig
|
||||
// moves, unless the user just typed something (1.5s grace window).
|
||||
useEffect(() => {
|
||||
const unsub = EventsOn('cat:state', (s: CATState) => {
|
||||
setCatState(s);
|
||||
if (!s?.connected) return;
|
||||
if (Date.now() < catFreezeUntilRef.current) return;
|
||||
const lk = locksRef.current;
|
||||
if (!lk.freq && s.freq_hz && s.freq_hz > 0) {
|
||||
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
||||
}
|
||||
// RX freq: backend follows ADIF — freq_hz = TX, freq_rx_hz = RX.
|
||||
// In split we take the rig's real RX freq; otherwise RX mirrors TX
|
||||
// (the user can still override it by hand). The freq lock covers both.
|
||||
if (!lk.freq) {
|
||||
if (s.split && s.freq_rx_hz && s.freq_rx_hz > 0) {
|
||||
setRxFreqMhz((s.freq_rx_hz / 1_000_000).toFixed(5));
|
||||
} else if (s.freq_hz && s.freq_hz > 0) {
|
||||
setRxFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
||||
}
|
||||
}
|
||||
if (!lk.band && s.band) setBand(s.band);
|
||||
|
||||
// Mode resolution priority:
|
||||
// 1. If freq matches a known digital watering hole, pick the specific
|
||||
// mode for that hole (FT8 / FT4) — beats whatever CAT reports.
|
||||
// 2. Else if CAT reports DATA (generic), use the user's configured
|
||||
// default digital mode (FT8 by default).
|
||||
// 3. Else trust CAT (SSB, CW, AM, FM…).
|
||||
if (!lk.mode) {
|
||||
const inferred = s.freq_hz ? inferDigitalMode(s.freq_hz) : '';
|
||||
let nextMode = '';
|
||||
if (inferred) nextMode = inferred;
|
||||
else if (s.mode === 'DATA') nextMode = digitalDefaultRef.current || 'FT8';
|
||||
else if (s.mode) nextMode = s.mode;
|
||||
if (nextMode) {
|
||||
setMode(nextMode);
|
||||
// Flip the RST default (599↔59) when the rig changes mode. Respects a
|
||||
// user-edited RST (applyModePreset early-returns when edited).
|
||||
applyModePreset(nextMode);
|
||||
}
|
||||
}
|
||||
});
|
||||
const unsub = EventsOn('cat:state', (s: CATState) => applyCatState(s));
|
||||
return () => { unsub?.(); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -1161,12 +1195,16 @@ export default function App() {
|
||||
const freqHz = freqMhz.trim() ? Math.round(parseFloat(freqMhz) * 1_000_000) : undefined;
|
||||
const rxFreqHz = rxFreqMhz.trim() ? Math.round(parseFloat(rxFreqMhz) * 1_000_000) : undefined;
|
||||
const now = new Date();
|
||||
const end = (locks.end && qsoEndedAt) ? qsoEndedAt : now;
|
||||
const baseStart = qsoStartedAt ?? now;
|
||||
// End: explicit when locked; for a deferred back-entry (start locked) it
|
||||
// defaults to the back-dated start so the QSO doesn't span to today;
|
||||
// otherwise it's now.
|
||||
const end = (locks.end && qsoEndedAt) ? qsoEndedAt : (locks.start ? baseStart : now);
|
||||
// Option: log TIME_ON = TIME_OFF (the moment the QSO completes). Useful
|
||||
// when you call a station for a long time — otherwise TIME_ON is frozen at
|
||||
// when you first entered the call (minutes early) and won't match LoTW.
|
||||
const startEqualsEnd = localStorage.getItem('opslog.startEqualsEnd') === '1';
|
||||
const start = (startEqualsEnd && !locks.start) ? end : (qsoStartedAt ?? now);
|
||||
const start = (startEqualsEnd && !locks.start) ? end : baseStart;
|
||||
const payload: any = {
|
||||
callsign: callsign.trim().toUpperCase(),
|
||||
qso_date: start.toISOString(),
|
||||
@@ -1407,6 +1445,11 @@ export default function App() {
|
||||
qsl_via: d.qsl_via || (r.qsl_via ?? ''),
|
||||
}));
|
||||
if (r.dxcc && r.dxcc > 0) runWorkedBefore(call, r.dxcc);
|
||||
// Begin the recording once the call resolves (a real, ≥3-char callsign,
|
||||
// not 1–2 stray letters). Covers the fast CW workflow (type → Enter to log
|
||||
// via the WinKeyer, no blur). No-op if the recorder is off or already
|
||||
// running; the pre-roll covers the lead-in.
|
||||
QSOAudioBegin().then(setRecording).catch(() => {});
|
||||
} catch (e: any) {
|
||||
setLookupResult(null);
|
||||
setLookupError(String(e?.message ?? e));
|
||||
@@ -1453,11 +1496,6 @@ export default function App() {
|
||||
// start is locked: the user is back-entering a past QSO and set a
|
||||
// specific time manually.
|
||||
setQsoStartedAt(new Date());
|
||||
// Begin the recording here too: a fast CW workflow (type the call then
|
||||
// hit Enter to log, exchanging with the WinKeyer) never blurs the call
|
||||
// field, so the blur-based start was missed. No-op if the recorder is off
|
||||
// or already running; the pre-roll covers the lead-in.
|
||||
QSOAudioBegin().then(setRecording).catch(() => {});
|
||||
} else if (isEmpty && !locks.start) {
|
||||
// Callsign wiped → user abandoned this QSO; reset the timer.
|
||||
setQsoStartedAt(null);
|
||||
@@ -1703,6 +1741,26 @@ export default function App() {
|
||||
<Combobox value={rstRcvd} options={rstOptions(mode, rstLists)} onChange={(v) => { setRstRcvd(v); rstUserEditedRef.current = true; }} />
|
||||
</div>
|
||||
);
|
||||
// Deferred-entry date: only shown when the start time is locked (back-entering
|
||||
// a past QSO). Sets the DATE part of qsoStartedAt; the time field keeps the time.
|
||||
const dateBlock = locks.start ? (
|
||||
<div className="flex flex-col w-40">
|
||||
<Label className="mb-1 h-3.5 text-emerald-700">Date</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={qsoStartedAt ? qsoStartedAt.toISOString().slice(0, 10) : ''}
|
||||
onChange={(e) => {
|
||||
const d = e.target.value; // YYYY-MM-DD
|
||||
if (!d) return;
|
||||
const [y, mo, da] = d.split('-').map(Number);
|
||||
const next = new Date(qsoStartedAt ?? new Date());
|
||||
next.setUTCFullYear(y, mo - 1, da);
|
||||
setQsoStartedAt(next);
|
||||
}}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
const startBlock = (
|
||||
<div className="flex flex-col w-28">
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1 text-emerald-700">Start UTC <LockBtn k="start" title="start time" /></Label>
|
||||
@@ -1728,7 +1786,12 @@ export default function App() {
|
||||
<Input
|
||||
readOnly={!locks.end}
|
||||
tabIndex={locks.end ? 0 : -1}
|
||||
value={endFocused ? endInputStr : (locks.end ? (qsoEndedAt ? fmtHMSUTC(qsoEndedAt) : '') : (qsoStartedAt ? utcNow.slice(11) : '—'))}
|
||||
value={endFocused ? endInputStr : (
|
||||
locks.end ? (qsoEndedAt ? fmtHMSUTC(qsoEndedAt) : '')
|
||||
// Deferred entry (start locked): the QSO is back-dated, so the end
|
||||
// follows the start instead of the live clock.
|
||||
: locks.start ? (qsoStartedAt ? fmtHMSUTC(qsoStartedAt) : '')
|
||||
: (qsoStartedAt ? utcNow.slice(11) : '—'))}
|
||||
onFocus={() => { setEndInputStr(qsoEndedAt ? fmtHMSUTC(qsoEndedAt) : ''); setEndFocused(true); }}
|
||||
onBlur={() => {
|
||||
setEndFocused(false);
|
||||
@@ -1812,18 +1875,7 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const cqBlock = (
|
||||
<div className="flex flex-col w-10"><Label className="mb-1 h-3.5 text-[10px]">CQ</Label>
|
||||
<Input inputMode="numeric" maxLength={2} className="font-mono text-center px-0.5 text-xs h-7" value={details.cqz ?? ''} placeholder="—"
|
||||
onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); updateDetails({ cqz: v === '' ? undefined : parseInt(v, 10) }); }} />
|
||||
</div>
|
||||
);
|
||||
const ituBlock = (
|
||||
<div className="flex flex-col w-10"><Label className="mb-1 h-3.5 text-[10px]">ITU</Label>
|
||||
<Input inputMode="numeric" maxLength={2} className="font-mono text-center px-0.5 text-xs h-7" value={details.ituz ?? ''} placeholder="—"
|
||||
onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); updateDetails({ ituz: v === '' ? undefined : parseInt(v, 10) }); }} />
|
||||
</div>
|
||||
);
|
||||
// CQ/ITU zones moved to the Info (F2) tab (DetailsPanel).
|
||||
const freqBlock = (
|
||||
<div className="flex flex-col w-32">
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1">{catState.split ? 'TX Freq (MHz)' : 'Freq (MHz)'} <LockBtn k="freq" title="frequency" /></Label>
|
||||
@@ -2243,14 +2295,14 @@ export default function App() {
|
||||
) : (
|
||||
/* Full Log4OM-style columnar layout. */
|
||||
<>
|
||||
{/* Row 1: Callsign + RST + CQ/ITU zones, then Start/End at right. */}
|
||||
{/* Row 1: Callsign + RST, then Start/End at right. CQ/ITU zones moved
|
||||
to the Info (F2) tab. */}
|
||||
<div className="flex gap-2 items-end">
|
||||
{callsignBlock}
|
||||
{rstTxBlock}
|
||||
{rstRxBlock}
|
||||
{cqBlock}
|
||||
{ituBlock}
|
||||
<div className="ml-auto flex gap-2">
|
||||
{dateBlock}
|
||||
{startBlock}
|
||||
{endBlock}
|
||||
</div>
|
||||
@@ -2303,6 +2355,7 @@ export default function App() {
|
||||
wbBusy={wbBusy}
|
||||
band={band}
|
||||
mode={mode}
|
||||
bands={bands}
|
||||
tab={detailTab}
|
||||
onTab={setDetailTab}
|
||||
keyerActive={(wkEnabled && wkStatus.connected) || dvkEnabled}
|
||||
@@ -2690,8 +2743,7 @@ export default function App() {
|
||||
onSpotClick={(s) => {
|
||||
const m = inferSpotMode(s.comment ?? '', s.freq_hz);
|
||||
if (catState.connected) {
|
||||
SetCATFrequency(s.freq_hz).catch(() => {});
|
||||
if (m) SetCATMode(m).catch(() => {});
|
||||
tuneRigCAT(s.freq_hz, m);
|
||||
} else {
|
||||
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
||||
if (s.band) setBand(s.band);
|
||||
@@ -2915,8 +2967,7 @@ export default function App() {
|
||||
onSpotClick={(s) => {
|
||||
const m = inferSpotMode(s.comment ?? '', s.freq_hz);
|
||||
if (catState.connected) {
|
||||
SetCATFrequency(s.freq_hz).catch(() => {});
|
||||
if (m) SetCATMode(m).catch(() => {});
|
||||
tuneRigCAT(s.freq_hz, m);
|
||||
} else {
|
||||
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
||||
if (s.band) setBand(s.band);
|
||||
@@ -2979,7 +3030,7 @@ export default function App() {
|
||||
})()}
|
||||
|
||||
{editingQSO && (
|
||||
<QSOEditModal qso={editingQSO} onSave={onModalSave} onDelete={onModalDelete} onClose={() => setEditingQSO(null)} countries={countries} />
|
||||
<QSOEditModal qso={editingQSO} onSave={onModalSave} onDelete={onModalDelete} onClose={() => setEditingQSO(null)} countries={countries} bands={bands} modes={modes} />
|
||||
)}
|
||||
|
||||
<SendSpotModal
|
||||
|
||||
Reference in New Issue
Block a user