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/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/**)"
]
}
}
+10
View File
@@ -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)
}
+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">
+3 -2
View File
@@ -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
</th>
{cols.map((b) => {
const st = statusMap.get(`${b.tag}|${cls}`) ?? '';
const isCurrent = b.tag === currentBand && classCurrent;
const isCurrent = hasCall && b.tag === currentBand && classCurrent;
return (
<td
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 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,
<div className="overflow-y-auto min-h-0">
{open === 'stats' && (
<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>
)}