diff --git a/.claude/settings.json b/.claude/settings.json index 62957fd..c714a6a 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -14,7 +14,9 @@ "Bash(ls \"/c/Program Files/Git/mingw64/bin/git-credential-manager\"*.exe)", "Bash(ls \"/c/Program Files/Git/mingw64/libexec/git-core/git-credential-manager\"*.exe)", "Bash(which git-credential-manager *)", - "Bash(gofmt -w internal/ultrabeam/ultrabeam.go)" + "Bash(gofmt -w internal/ultrabeam/ultrabeam.go)", + "Bash(cd \"c:/Perso/Seafile/Programmation/Golang/OpsLog/frontend\" && npx tsc --noEmit 2>&1 | grep -v \"npm notice\" | head && cd .. && /c/Users/legre/go/bin/wails build 2>&1 | tail -2 && ls -la --time-style=+%H:%M build/bin/OpsLog.exe)", + "Read(//c/Perso/Seafile/Programmation/Golang/**)" ] } } diff --git a/app.go b/app.go index 70e8c7c..644538b 100644 --- a/app.go +++ b/app.go @@ -6320,6 +6320,16 @@ func (a *App) SetUltrabeamDirection(direction int) error { if direction < 0 || direction > 2 { return fmt.Errorf("invalid direction %d", direction) } + // The device has no standalone direction command: it re-issues the current + // frequency with the new direction byte. If the antenna hasn't reported a + // frequency yet (just connected / remote link still settling), fall back to + // the rig's current CAT frequency so the control still works. + st, _ := a.ultrabeam.GetStatus() + if (st == nil || st.Frequency <= 0) && a.cat != nil { + if rs := a.cat.State(); rs.Connected && rs.FreqHz > 0 { + return a.ultrabeam.SetFrequency(int(rs.FreqHz/1000), direction) + } + } return a.ultrabeam.SetDirection(direction) } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index caaf466..59d6448 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -548,11 +548,9 @@ export default function App() { const [clusterServerStatuses, setClusterServerStatuses] = useState([]); // "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(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 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))); - // in a macro (e.g. "73 TU ") logs the contact after sending. - // resolveCW already strips the token from the keyed text (unknown var → ""). - if (//i.test(rawText)) void save(); + const doLog = //i.test(rawText); // resolveCW strips the token (unknown var → "") + await WinkeyerSend(resolveCW(rawText)).catch((e) => setError(String(e?.message ?? e))); + // (e.g. "BK 73 TU ") 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 && ( -
- - - Spotted by {selfSpot.spotter || '?'} - {selfSpot.comment ? with {selfSpot.comment} : null} - -
- -
- )} {/* ===== ENTRY STRIP ===== Enter from any inside the strip logs the QSO. Radix Selects @@ -2588,7 +2572,6 @@ export default function App() { Main Recent QSOs - {qsos.length} Cluster diff --git a/frontend/src/components/BandSlotGrid.tsx b/frontend/src/components/BandSlotGrid.tsx index 3935cd5..227a15c 100644 --- a/frontend/src/components/BandSlotGrid.tsx +++ b/frontend/src/components/BandSlotGrid.tsx @@ -12,6 +12,7 @@ interface Props { currentBand: string; currentMode: string; bands?: string[]; // operator's configured bands; falls back to DEFAULT_BANDS + hasCall?: boolean; // a callsign is being entered — only then highlight the "current entry" cell } // Compact column label for a band tag: keep the classic V/U for 2m/70cm, @@ -78,7 +79,7 @@ function cellTitle(band: string, cls: string, status: string, current: boolean): return `${band} ${cls}: ${desc}${current ? ' — current entry' : ''}`; } -export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands }: Props) { +export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCall = true }: Props) { // Columns from the operator's configured bands (so the matrix shows only the // bands they actually use), falling back to the built-in default set. const cols = useMemo( @@ -181,7 +182,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands }: Prop {cols.map((b) => { const st = statusMap.get(`${b.tag}|${cls}`) ?? ''; - const isCurrent = b.tag === currentBand && classCurrent; + const isCurrent = hasCall && b.tag === currentBand && classCurrent; return ( ('stats'); const open = tab ?? internalOpen; // controlled when `tab` is provided // Bearing/distance from operator's home grid to the remote station. @@ -182,7 +182,7 @@ export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid,
{open === 'stats' && (
- +
)}