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
+3 -1
View File
@@ -14,7 +14,9 @@
"Bash(ls \"/c/Program Files/Git/mingw64/bin/git-credential-manager\"*.exe)", "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(ls \"/c/Program Files/Git/mingw64/libexec/git-core/git-credential-manager\"*.exe)",
"Bash(which git-credential-manager *)", "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/**)"
] ]
} }
} }
+10
View File
@@ -6320,6 +6320,16 @@ func (a *App) SetUltrabeamDirection(direction int) error {
if direction < 0 || direction > 2 { if direction < 0 || direction > 2 {
return fmt.Errorf("invalid direction %d", direction) 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) return a.ultrabeam.SetDirection(direction)
} }
+21 -38
View File
@@ -548,11 +548,9 @@ export default function App() {
const [clusterServerStatuses, setClusterServerStatuses] = useState<ServerStatus[]>([]); const [clusterServerStatuses, setClusterServerStatuses] = useState<ServerStatus[]>([]);
// "Send DX spot" dialog (Log4OM-style) — only reachable when a cluster is up. // "Send DX spot" dialog (Log4OM-style) — only reachable when a cluster is up.
const [showSpotModal, setShowSpotModal] = useState(false); const [showSpotModal, setShowSpotModal] = useState(false);
// "You have been spotted" banner — set when a cluster spot's DX call is our // Holds our station callsign for the (one-shot) cluster spot listener, so a
// own station callsign. Ref holds our call for the (one-shot) spot listener. // self-spot can be surfaced in the shared header toast.
const [selfSpot, setSelfSpot] = useState<{ spotter: string; freqKHz: number; band?: string; comment?: string; at: number } | null>(null);
const myCallRef = useRef(''); const myCallRef = useRef('');
const selfSpotTimerRef = useRef<number | null>(null);
// === WinKeyer CW keyer === // === WinKeyer CW keyer ===
const [wkEnabled, setWkEnabled] = useState(false); 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. // F1-F12 macro shortcuts active only when the keyer is enabled + connected.
const wkActiveRef = useRef(false); const wkActiveRef = useRef(false);
const wkEscClearsRef = useRef(true); 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(() => { wkActiveRef.current = wkEnabled && wkStatus.connected; }, [wkEnabled, wkStatus.connected]);
useEffect(() => { wkEscClearsRef.current = wkEscClears; }, [wkEscClears]); useEffect(() => { wkEscClearsRef.current = wkEscClears; }, [wkEscClears]);
@@ -1074,13 +1074,13 @@ export default function App() {
const next = [sp, ...arr]; const next = [sp, ...arr];
return next.length > SPOTS_CAP ? next.slice(0, SPOTS_CAP) : next; 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; const mine = myCallRef.current;
if (mine && (sp.dx_call ?? '').toUpperCase() === mine) { 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() }); const by = cleanSpotter(sp.spotter ?? '') || '?';
// Auto-hide 3 s after the last self-spot; a new one resets the timer. const c = (sp.comment ?? '').trim();
if (selfSpotTimerRef.current) window.clearTimeout(selfSpotTimerRef.current); showToast(`Spotted by ${by}${c ? ` with ${c}` : ''}`);
selfSpotTimerRef.current = window.setTimeout(() => setSelfSpot(null), 3000);
} }
}); });
return () => { unsubState?.(); unsubSpot?.(); }; return () => { unsubState?.(); unsubSpot?.(); };
@@ -1208,12 +1208,18 @@ export default function App() {
} }
return out.replace(/\s+/g, ' ').trim(); return out.replace(/\s+/g, ' ').trim();
} }
function wkSend(rawText: string) { async function wkSend(rawText: string) {
setWkSent(''); setWkSent('');
WinkeyerSend(resolveCW(rawText)).catch((e) => setError(String(e?.message ?? e))); const doLog = /<LOGQSO>/i.test(rawText); // resolveCW strips the token (unknown var → "")
// <LOGQSO> in a macro (e.g. "73 TU <LOGQSO>") logs the contact after sending. await WinkeyerSend(resolveCW(rawText)).catch((e) => setError(String(e?.message ?? e)));
// resolveCW already strips the token from the keyed text (unknown var → ""). // <LOGQSO> (e.g. "BK 73 TU <LOGQSO>") logs the contact AFTER the keyer has
if (/<LOGQSO>/i.test(rawText)) void save(); // 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); } function wkSendMacro(i: number) { const m = wkMacros[i]; if (m) wkSend(m.text); }
wkSendMacroRef.current = wkSendMacro; wkSendMacroRef.current = wkSendMacro;
@@ -1300,16 +1306,10 @@ export default function App() {
email: details.email, email: details.email,
}; };
applyAwardRefs(payload, details.award_refs ?? '', awardFieldRef.current); applyAwardRefs(payload, details.award_refs ?? '', awardFieldRef.current);
const loggedCall = String(payload.callsign ?? '');
const loggedDxcc = typeof payload.dxcc === 'number' ? payload.dxcc : 0;
await AddQSO(payload); 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) callsignRef.current?.focus(); // return focus to the call field, wherever it was (e.g. Name)
await refresh(); 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) { } catch (e: any) {
setError(String(e?.message ?? e)); setError(String(e?.message ?? e));
} finally { setSaving(false); } } 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 ===== {/* ===== ENTRY STRIP =====
Enter from any <input> inside the strip logs the QSO. Radix Selects 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="main">Main</TabsTrigger>
<TabsTrigger value="recent"> <TabsTrigger value="recent">
Recent QSOs Recent QSOs
<Badge variant="secondary" className="ml-1.5 px-1.5 py-0 text-[10px]">{qsos.length}</Badge>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="cluster">Cluster</TabsTrigger> <TabsTrigger value="cluster">Cluster</TabsTrigger>
<TabsTrigger value="worked"> <TabsTrigger value="worked">
+3 -2
View File
@@ -12,6 +12,7 @@ interface Props {
currentBand: string; currentBand: string;
currentMode: string; currentMode: string;
bands?: string[]; // operator's configured bands; falls back to DEFAULT_BANDS 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, // 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' : ''}`; 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 // 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. // bands they actually use), falling back to the built-in default set.
const cols = useMemo( const cols = useMemo(
@@ -181,7 +182,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands }: Prop
</th> </th>
{cols.map((b) => { {cols.map((b) => {
const st = statusMap.get(`${b.tag}|${cls}`) ?? ''; const st = statusMap.get(`${b.tag}|${cls}`) ?? '';
const isCurrent = b.tag === currentBand && classCurrent; const isCurrent = hasCall && b.tag === currentBand && classCurrent;
return ( return (
<td <td
key={b.tag} key={b.tag}
+2 -2
View File
@@ -118,7 +118,7 @@ function Field({ label, span = 1, children }: { label: string; span?: 1 | 2 | 3
); );
} }
export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid, details, onChange, wb, wbBusy, band, mode, bands, tab, onTab, keyerActive }: Props) { export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, details, onChange, wb, wbBusy, band, mode, bands, tab, onTab, keyerActive }: Props) {
const [internalOpen, setInternalOpen] = useState<TabName>('stats'); const [internalOpen, setInternalOpen] = useState<TabName>('stats');
const open = tab ?? internalOpen; // controlled when `tab` is provided const open = tab ?? internalOpen; // controlled when `tab` is provided
// Bearing/distance from operator's home grid to the remote station. // Bearing/distance from operator's home grid to the remote station.
@@ -182,7 +182,7 @@ export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid,
<div className="overflow-y-auto min-h-0"> <div className="overflow-y-auto min-h-0">
{open === 'stats' && ( {open === 'stats' && (
<div className="px-3 py-2.5"> <div className="px-3 py-2.5">
<BandSlotGrid wb={wb} busy={!!wbBusy} currentBand={band} currentMode={mode} bands={bands} /> <BandSlotGrid wb={wb} busy={!!wbBusy} currentBand={band} currentMode={mode} bands={bands} hasCall={callsign.trim() !== ''} />
</div> </div>
)} )}