Skip to content

Commit bc47f62

Browse files
authored
Merge pull request #833 from s53zo/fix/contest-qso-dx-target
Fix contest QSO DX target updates
2 parents a79aa9b + 0c6ea88 commit bc47f62

File tree

7 files changed

+64
-11
lines changed

7 files changed

+64
-11
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
push:
55
branches: [main, develop]
66
pull_request:
7-
branches: [main]
7+
branches: [main, Staging]
88

99
permissions:
1010
contents: read

AddOns/APRS-Newsfeed/aprs_newsfeed.user.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,13 @@
160160
let lastUpdateTs = parseInt(localStorage.getItem('ohc_aprs_last_update')) || 0;
161161

162162
// Escape HTML to prevent XSS when interpolating into innerHTML
163-
const esc = (s) => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
163+
const esc = (s) =>
164+
String(s)
165+
.replace(/&/g, '&amp;')
166+
.replace(/</g, '&lt;')
167+
.replace(/>/g, '&gt;')
168+
.replace(/"/g, '&quot;')
169+
.replace(/'/g, '&#39;');
164170

165171
function getCallsign() {
166172
try {

docs/N1MM-SETUP.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ If you're running directly with Node, restart the server after changing your `.e
5353

5454
All settings go in your `.env` or `.env.local` file:
5555

56-
| Variable | Default | Description |
57-
|----------|---------|-------------|
58-
| `N1MM_UDP_ENABLED` | `false` | Set to `true` to enable the UDP listener |
59-
| `N1MM_UDP_PORT` | `12060` | UDP port to listen on (must match N1MM+ config) |
60-
| `N1MM_MAX_QSOS` | `200` | Maximum QSOs to keep in memory |
61-
| `N1MM_QSO_MAX_AGE_MINUTES` | `360` | QSOs older than this (6 hours) are pruned automatically |
56+
| Variable | Default | Description |
57+
| -------------------------- | ------- | ------------------------------------------------------- |
58+
| `N1MM_UDP_ENABLED` | `false` | Set to `true` to enable the UDP listener |
59+
| `N1MM_UDP_PORT` | `12060` | UDP port to listen on (must match N1MM+ config) |
60+
| `N1MM_MAX_QSOS` | `200` | Maximum QSOs to keep in memory |
61+
| `N1MM_QSO_MAX_AGE_MINUTES` | `360` | QSOs older than this (6 hours) are pruned automatically |
6262

6363
## Docker Users
6464

dxspider-proxy/server.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,10 @@ const handleDisconnect = () => {
447447
// Detect rapid disconnect (kicked within seconds of connecting)
448448
const connectionDuration = connectionStartTime ? Date.now() - connectionStartTime.getTime() : 0;
449449
if (connectionDuration > 0 && connectionDuration < 15000) {
450-
log('RECONNECT', `Rapid disconnect from ${currentNode?.name} after ${Math.round(connectionDuration / 1000)}s (likely auth rejection or SSID conflict)`);
450+
log(
451+
'RECONNECT',
452+
`Rapid disconnect from ${currentNode?.name} after ${Math.round(connectionDuration / 1000)}s (likely auth rejection or SSID conflict)`,
453+
);
451454
}
452455

453456
reconnectAttempts++;
@@ -456,7 +459,10 @@ const handleDisconnect = () => {
456459
// Try next node
457460
currentNodeIndex = (currentNodeIndex + 1) % CONFIG.nodes.length;
458461
reconnectAttempts = 0;
459-
log('FAILOVER', `${CONFIG.maxReconnectAttempts} consecutive failures — switching to node: ${CONFIG.nodes[currentNodeIndex].name}`);
462+
log(
463+
'FAILOVER',
464+
`${CONFIG.maxReconnectAttempts} consecutive failures — switching to node: ${CONFIG.nodes[currentNodeIndex].name}`,
465+
);
460466
}
461467

462468
log('RECONNECT', `Attempting reconnect in ${CONFIG.reconnectDelayMs}ms (attempt ${reconnectAttempts})`);

src/components/PluginLayer.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const PluginLayerInner = ({
2727
enabled,
2828
opacity,
2929
map,
30+
onDXChange,
3031
mapBandFilter,
3132
callsign,
3233
locator,
@@ -43,6 +44,7 @@ const PluginLayerInner = ({
4344
map: safeMap,
4445
enabled,
4546
opacity,
47+
onDXChange,
4648
callsign,
4749
locator,
4850
mapBandFilter,

src/components/WorldMap.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1925,6 +1925,7 @@ export const WorldMap = ({
19251925
plugin={layerDef}
19261926
enabled={pluginLayerStates[layerDef.id]?.enabled ?? layerDef.defaultEnabled}
19271927
opacity={pluginLayerStates[layerDef.id]?.opacity ?? layerDef.defaultOpacity}
1928+
onDXChange={onDXChange}
19281929
mapBandFilter={mapBandFilter}
19291930
config={pluginLayerStates[layerDef.id]?.config ?? layerDef.config}
19301931
map={isAzimuthal ? azimuthalMapRef.current : mapInstanceRef.current}

src/plugins/layers/useContestQsos.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,26 @@ const bandFromAnyFrequency = (freq) => {
3030
return normalizeBandKey(getBandFromFreq(n));
3131
};
3232

33-
export function useLayer({ enabled = false, opacity = 0.7, map = null, mapBandFilter }) {
33+
const findLatestLocatedQso = (qsos) => {
34+
for (let i = qsos.length - 1; i >= 0; i -= 1) {
35+
const qso = qsos[i];
36+
const lat = parseFloat(qso?.lat);
37+
const lon = parseFloat(qso?.lon);
38+
if (Number.isFinite(lat) && Number.isFinite(lon)) {
39+
return { qso, lat, lon };
40+
}
41+
}
42+
return null;
43+
};
44+
45+
export function useLayer({ enabled = false, opacity = 0.7, map = null, onDXChange, mapBandFilter }) {
3446
const [qsos, setQsos] = useState([]);
3547
const [deLocation, setDeLocation] = useState(null);
3648
const markersRef = useRef([]);
3749
const linesRef = useRef([]);
3850
const pollRef = useRef(null);
3951
const configLoadedRef = useRef(false);
52+
const lastHandledTargetKeyRef = useRef(null);
4053

4154
useEffect(() => {
4255
if (!enabled || configLoadedRef.current) return;
@@ -80,6 +93,31 @@ export function useLayer({ enabled = false, opacity = 0.7, map = null, mapBandFi
8093
};
8194
}, [enabled]);
8295

96+
useEffect(() => {
97+
if (!enabled) {
98+
lastHandledTargetKeyRef.current = null;
99+
}
100+
}, [enabled]);
101+
102+
useEffect(() => {
103+
if (!enabled || typeof onDXChange !== 'function') return;
104+
105+
const latestLocated = findLatestLocatedQso(qsos);
106+
if (!latestLocated) return;
107+
108+
const { qso: latestLocatedQso, lat, lon } = latestLocated;
109+
const targetKey =
110+
latestLocatedQso.id ||
111+
`${latestLocatedQso.timestamp || ''}:${latestLocatedQso.dxCall || ''}:${lat.toFixed(4)}:${lon.toFixed(4)}`;
112+
113+
if (lastHandledTargetKeyRef.current === targetKey) return;
114+
115+
// Remember handled QSOs even while DX is locked so unlocking later
116+
// does not retroactively replay an older contest contact.
117+
lastHandledTargetKeyRef.current = targetKey;
118+
onDXChange({ lat, lon });
119+
}, [enabled, qsos, onDXChange]);
120+
83121
useEffect(() => {
84122
if (!map || typeof L === 'undefined') return;
85123

0 commit comments

Comments
 (0)