This commit is contained in:
2026-06-13 17:25:48 +02:00
parent d3ba7c71f4
commit 0b3e22c97e
5 changed files with 39 additions and 43 deletions
+21 -38
View File
@@ -548,11 +548,9 @@ export default function App() {
const [clusterServerStatuses, setClusterServerStatuses] = useState<ServerStatus[]>([]);
// "Send DX spot" dialog (Log4OM-style) — only reachable when a cluster is up.
const [showSpotModal, setShowSpotModal] = useState(false);
// "You have been spotted" banner — set when a cluster spot's DX call is our
// own station callsign. Ref holds our call for the (one-shot) spot listener.
const [selfSpot, setSelfSpot] = useState<{ spotter: string; freqKHz: number; band?: string; comment?: string; at: number } | null>(null);
// Holds our station callsign for the (one-shot) cluster spot listener, so a
// self-spot can be surfaced in the shared header toast.
const myCallRef = useRef('');
const selfSpotTimerRef = useRef<number | null>(null);
// === WinKeyer CW keyer ===
const [wkEnabled, setWkEnabled] = useState(false);
@@ -567,6 +565,8 @@ export default function App() {
// F1-F12 macro shortcuts active only when the keyer is enabled + connected.
const wkActiveRef = useRef(false);
const wkEscClearsRef = useRef(true);
const wkBusyRef = useRef(false); // live "keyer is sending" flag, for the <LOGQSO> wait-then-log
useEffect(() => { wkBusyRef.current = wkStatus.busy; }, [wkStatus.busy]);
useEffect(() => { wkActiveRef.current = wkEnabled && wkStatus.connected; }, [wkEnabled, wkStatus.connected]);
useEffect(() => { wkEscClearsRef.current = wkEscClears; }, [wkEscClears]);
@@ -1074,13 +1074,13 @@ export default function App() {
const next = [sp, ...arr];
return next.length > SPOTS_CAP ? next.slice(0, SPOTS_CAP) : next;
});
// Self-spot: someone spotted OUR callsign on the cluster.
// Self-spot: someone spotted OUR callsign — show it in the shared header
// toast (same place as the other notifications), not a separate banner.
const mine = myCallRef.current;
if (mine && (sp.dx_call ?? '').toUpperCase() === mine) {
setSelfSpot({ spotter: cleanSpotter(sp.spotter ?? ''), freqKHz: sp.freq_khz, band: sp.band, comment: sp.comment, at: Date.now() });
// Auto-hide 3 s after the last self-spot; a new one resets the timer.
if (selfSpotTimerRef.current) window.clearTimeout(selfSpotTimerRef.current);
selfSpotTimerRef.current = window.setTimeout(() => setSelfSpot(null), 3000);
const by = cleanSpotter(sp.spotter ?? '') || '?';
const c = (sp.comment ?? '').trim();
showToast(`Spotted by ${by}${c ? ` with ${c}` : ''}`);
}
});
return () => { unsubState?.(); unsubSpot?.(); };
@@ -1208,12 +1208,18 @@ export default function App() {
}
return out.replace(/\s+/g, ' ').trim();
}
function wkSend(rawText: string) {
async function wkSend(rawText: string) {
setWkSent('');
WinkeyerSend(resolveCW(rawText)).catch((e) => setError(String(e?.message ?? e)));
// <LOGQSO> in a macro (e.g. "73 TU <LOGQSO>") logs the contact after sending.
// resolveCW already strips the token from the keyed text (unknown var → "").
if (/<LOGQSO>/i.test(rawText)) void save();
const doLog = /<LOGQSO>/i.test(rawText); // resolveCW strips the token (unknown var → "")
await WinkeyerSend(resolveCW(rawText)).catch((e) => setError(String(e?.message ?? e)));
// <LOGQSO> (e.g. "BK 73 TU <LOGQSO>") logs the contact AFTER the keyer has
// finished sending — wait for the busy flag to rise then fall, so the QSO
// isn't logged (and the form cleared) while the CW is still going out.
if (!doLog) return;
const sleep = (ms: number) => new Promise((r) => window.setTimeout(r, ms));
for (let i = 0; i < 20 && !wkBusyRef.current; i++) await sleep(50); // ≤1s for sending to start
for (let i = 0; i < 1200 && wkBusyRef.current; i++) await sleep(50); // ≤60s for it to finish
void save();
}
function wkSendMacro(i: number) { const m = wkMacros[i]; if (m) wkSend(m.text); }
wkSendMacroRef.current = wkSendMacro;
@@ -1300,16 +1306,10 @@ export default function App() {
email: details.email,
};
applyAwardRefs(payload, details.award_refs ?? '', awardFieldRef.current);
const loggedCall = String(payload.callsign ?? '');
const loggedDxcc = typeof payload.dxcc === 'number' ? payload.dxcc : 0;
await AddQSO(payload);
resetEntry();
resetEntry(); // clears the call AND the Worked-before matrix
callsignRef.current?.focus(); // return focus to the call field, wherever it was (e.g. Name)
await refresh();
// Refresh the Worked-before matrix so the just-logged band/mode flips to
// "worked" — resetEntry cleared it, so re-fetch for the logged call (a
// live DB query, so it now includes this QSO).
if (loggedCall.length >= 3) runWorkedBefore(loggedCall, loggedDxcc);
} catch (e: any) {
setError(String(e?.message ?? e));
} finally { setSaving(false); }
@@ -2378,22 +2378,6 @@ export default function App() {
)}
{/* "You have been spotted" banner — shows when our own callsign appears
in a cluster spot. Floated top-centre (with the other notifications),
never shifts the layout; auto-hides 3s after the last self-spot. */}
{!compact && selfSpot && (
<div className="fixed top-12 left-1/2 -translate-x-1/2 z-[100] flex items-center gap-2 rounded-lg border border-amber-300 bg-amber-100 text-amber-900 px-3.5 py-2 text-xs shadow-lg animate-in fade-in slide-in-from-top-2">
<RadioTower className="size-3.5 shrink-0" />
<span>
Spotted by <strong className="font-mono">{selfSpot.spotter || '?'}</strong>
{selfSpot.comment ? <span className="text-amber-800"> with {selfSpot.comment}</span> : null}
</span>
<div className="flex-1" />
<button className="text-amber-700 hover:text-amber-900" title="Dismiss" onClick={() => setSelfSpot(null)}>
<X className="size-3.5" />
</button>
</div>
)}
{/* ===== ENTRY STRIP =====
Enter from any <input> inside the strip logs the QSO. Radix Selects
@@ -2588,7 +2572,6 @@ export default function App() {
<TabsTrigger value="main">Main</TabsTrigger>
<TabsTrigger value="recent">
Recent QSOs
<Badge variant="secondary" className="ml-1.5 px-1.5 py-0 text-[10px]">{qsos.length}</Badge>
</TabsTrigger>
<TabsTrigger value="cluster">Cluster</TabsTrigger>
<TabsTrigger value="worked">