feat(cluster): a SOTA column, and a spot click that credits the summit

The SOTA feeds put the summit in the spot's own text, so there is nothing to
poll: a regex on the comment gives the reference, and clicking the spot writes
SOTA@<ref> into the award references the way a park already wrote POTA@<ref>.

The regex is narrow on purpose — association/region-NNN, anchored — because
POTA (US-4475), WWFF (DLFF-0001) and portable callsigns (DL/SP9DPM/P) share
that field and none of them is a summit. Pinned by a table test.

Off by default: the reference is only there on the summit feeds, and an empty
column for every operator who does not chase them is worse than a checkbox.
This commit is contained in:
2026-08-27 21:33:17 +02:00
parent b8796369ba
commit 08d316b1e0
9 changed files with 108 additions and 36 deletions
+15 -11
View File
@@ -3275,7 +3275,7 @@ export default function App() {
function handleSpotSelect(s: any) {
if (!s?.dx_call?.trim()) return;
onCallsignInput(s.dx_call, { force: true });
applySpotPOTA((s as any).pota_ref);
applySpotRefs((s as any).pota_ref, (s as any).sota_ref);
}
function handleSpotClick(s: any) {
@@ -3300,7 +3300,7 @@ export default function App() {
FlexZoomForSpot(m ?? '', s.freq_hz ?? 0).catch(() => {});
if (m) applyModeFromSpot(m);
onCallsignInput(s.dx_call, { force: true });
applySpotPOTA((s as any).pota_ref);
applySpotRefs((s as any).pota_ref, (s as any).sota_ref);
if (s.dx_call?.trim()) restartRecordingForNewTarget(s.dx_call);
}
@@ -3761,14 +3761,14 @@ export default function App() {
restartRecordingForNewTarget(call);
// The park, like a click in the band map: the radio reports only a
// callsign, so the backend looks it up again before sending the event.
applySpotPOTA(String(p?.pota_ref ?? ''));
applySpotRefs(String(p?.pota_ref ?? ''), String(p?.sota_ref ?? ''));
});
// Clicking a spot on the ExpertSDR (TCI) panorama fills the call, like Flex.
const unsubTciSpot = EventsOn('tci:spot_clicked', (p: any) => {
const call = String(p?.call ?? '');
if (!applyUdpCall(call, true)) return;
restartRecordingForNewTarget(call);
applySpotPOTA(String(p?.pota_ref ?? ''));
applySpotRefs(String(p?.pota_ref ?? ''), String(p?.sota_ref ?? ''));
});
const unsubBulk = EventsOn('bulkupdate:progress', (p: any) => {
const total = Number(p?.total ?? 0);
@@ -4796,13 +4796,17 @@ export default function App() {
}
wbTimerRef.current = window.setTimeout(() => runWorkedBefore(call), 150);
}
// applySpotPOTA sets the QSO's POTA award reference(s) from a clicked spot's
// park ref ("US-4164" or n-fer "US-1,US-2"). Empty ref clears it (fresh
// target). Routed to the pota_ref column at save via applyAwardRefs.
function applySpotPOTA(potaRef?: string) {
const refs = String(potaRef || '')
.split(/[,;]/).map((x) => x.trim().toUpperCase()).filter(Boolean);
setDetails((d) => ({ ...d, award_refs: refs.map((r) => `POTA@${r}`).join(';') }));
// Award references carried by the spot itself: the park a station is
// activating, the summit it is on, or both. Written into award_refs the same
// way for each, so logging the contact credits it without retyping a
// reference that was on screen.
function applySpotRefs(potaRef?: string, sotaRef?: string) {
const split = (v?: string) => String(v || '').split(/[,;]/).map((x) => x.trim().toUpperCase()).filter(Boolean);
const refs = [
...split(potaRef).map((r) => `POTA@${r}`),
...split(sotaRef).map((r) => `SOTA@${r}`),
];
setDetails((d) => ({ ...d, award_refs: refs.join(';') }));
}
function onCallsignInput(v: string, opts?: { force?: boolean }) {
// Programmatic call-sets (force: spot click, UDP, external app) count as