fix
This commit is contained in:
@@ -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/**)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user