Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1ac7877
Merge pull request #956 from accius/Staging
accius May 7, 2026
68ebe67
Merge pull request #957 from accius/Staging
accius May 7, 2026
91b03f8
Merge pull request #965 from MichaelWheeley/bugfix_use_lightning_line787
accius May 10, 2026
587c2cb
Merge pull request #968 from accius/Staging
accius May 10, 2026
08e0898
Merge pull request #969 from accius/Staging
accius May 10, 2026
2bbaa2a
Release v26.3.3 (#977)
accius May 12, 2026
e95faf6
fix(dxspider-proxy): stop quiet-band connection churn + fix auth dete…
accius May 20, 2026
96f4bfe
Revert "fix(dxspider-proxy): stop quiet-band connection churn + fix a…
accius May 20, 2026
b950dc5
Merge pull request #1052 from accius/Staging
accius Jun 2, 2026
2a3b763
Merge pull request #1055 from accius/Staging
accius Jun 2, 2026
6b5fae1
Merge pull request #1056 from accius/Staging
accius Jun 2, 2026
d103a7b
fix(stats): resolve real client IP via CF-Connecting-IP behind Cloudf…
accius Jun 5, 2026
40793b9
Merge pull request #1067 from accius/cherry-pick/cf-connecting-ip-prod
accius Jun 5, 2026
b7ee2f1
fix(dxcluster): stop hammering cluster nodes with invalid login
accius Jun 10, 2026
b0ada16
Fix N3FJP logged 0,0 bug, add background bridge script, and add npm r…
stearnsy33 Jun 19, 2026
58b09bb
Create n3fjp-bridge
stearnsy33 Jun 21, 2026
3e7cdcf
Update config-routes.js
stearnsy33 Jun 21, 2026
1234100
Update SettingsPanel.jsx
stearnsy33 Jun 21, 2026
64eebe3
Rename n3fjp-bridge to n3fjp-bridge.js
stearnsy33 Jun 21, 2026
7c4ca33
Merge branch 'Staging' into feature-n3fjp-integration
stearnsy33 Jun 22, 2026
499ef01
Refactor: extract N3FJP constants, add ADIF bounds check, and impleme…
stearnsy33 Jun 24, 2026
32cce12
Fix: read N3FJP host and port dynamically from environment variables
stearnsy33 Jun 24, 2026
b69e378
Fix: resolve duplicate variable declaration for socket client
stearnsy33 Jun 24, 2026
7232e5f
Fix: fetch dynamic N3FJP host and port from settings API at startup
stearnsy33 Jun 24, 2026
700ab3c
Fix: address code review by fetching dynamic N3FJP host and port from…
stearnsy33 Jun 24, 2026
4d367e8
Merge branch 'feature-n3fjp-ui' into feature-n3fjp-integration
stearnsy33 Jun 25, 2026
e4844b4
Build: push compiled assets for Surface deployment
stearnsy33 Jun 25, 2026
50f099b
feat: resolve N3FJP loopback issue by passing dynamic UI configuratio…
stearnsy33 Jun 25, 2026
e73c674
Clean up dist folder and apply SettingsPanel syntax fix
stearnsy33 Jun 25, 2026
0f06a76
Add architecture comment and fix N3FJP route
stearnsy33 Jun 25, 2026
24b217a
Fix mojibake encoding in config-routes.js
stearnsy33 Jun 30, 2026
426032d
Restore /api/version, fix encoding symbols, and finalize config-route…
stearnsy33 Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"electron-builder": "electron-builder",
"prebuild": "node scripts/fetch-wasm.js",
"build": "npx vite build",
"bridge:n3fjp": "node scripts/n3fjp-bridge.js",
"lang:check": "node scripts/lang-check-json-key-sort.js",
"lang:sort": "node scripts/lang-json-key-sort.mjs",
"preview": "npx vite preview",
Expand Down
150 changes: 150 additions & 0 deletions scripts/n3fjp-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
const net = require('net');
const http = require('http');

const N3FJP_PORT = 1100;
const OHC_HOST = '127.0.0.1';
const OHC_PORT = 3001;

const client = new net.Socket();
client.setNoDelay(true); // Kills network buffering lag

client.connect(N3FJP_PORT, '127.0.0.1', () => {
console.log('✅ Bridge Connected to N3FJP (Low-Latency Mode)');
});

let dataBuffer = '';

client.on('data', (data) => {
dataBuffer += data.toString();
while (dataBuffer.includes('</CMD>')) {
const endIdx = dataBuffer.indexOf('</CMD>') + 6;
const currentRecord = dataBuffer.substring(0, endIdx);
dataBuffer = dataBuffer.substring(endIdx);
processN3FJPRecord(currentRecord);
}
});

// Utility to convert ADIF format (e.g., N044 38.5 or W070 12.3) to Decimal Degrees
function parseAdifCoords(rawStr, isLongitude) {
if (!rawStr) return 0;
const clean = rawStr.toUpperCase().trim();
if (!clean) return 0;

const match = clean.match(/^([NSEW])\s*(\d+)(?:\s+([\d.]+))?/);
if (!match) {
const val = parseFloat(clean);
return Number.isFinite(val) ? val : 0;
}

const dir = match[1];
const degrees = parseInt(match[2], 10);
const minutes = match[3] ? parseFloat(match[3]) : 0;

let decimal = degrees + minutes / 60;

if (dir === 'S' || dir === 'W') {
decimal = -decimal;
}

return decimal;
}

// Quick helper to fetch true callsign coordinates from your existing server database
function fetchTrueCallCoords(callsign) {
return new Promise((resolve) => {
const call = (callsign || '').toUpperCase().trim();
if (!call || call === 'CLEAR') return resolve(null);

const req = http.get(`http://${OHC_HOST}:${OHC_PORT}/api/callsign/${encodeURIComponent(call)}`, (res) => {
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
try {
const data = JSON.parse(body);
if (data && typeof data.lat === 'number' && typeof data.lon === 'number') {
return resolve({ lat: data.lat, lon: data.lon });
}
} catch (e) {}
resolve(null);
});
});
req.on('error', () => resolve(null));
req.end();
});
}

async function processN3FJPRecord(raw) {
const getTag = (tag) => {
const m = raw.match(new RegExp(`<${tag}>(.*?)</${tag}>`, 'i'));
return m && m[1] ? m[1].trim() : '';
};

const call = getTag('CALL');
const isClearSignal = raw.includes('CLEARTAB') || raw.includes('<CLEAR>') || (raw.includes('CALLTAB') && call === '');

let eventType = isClearSignal ? 'clear' : raw.includes('CALLTAB') ? 'preview' : 'log';

if (!call && eventType !== 'clear') return;

// Parse the raw incoming coordinates out of N3FJP
const rawLat = getTag('LAT');
const rawLon = getTag('LON');
let lat = parseAdifCoords(rawLat, false);
let lon = parseAdifCoords(rawLon, true);

// 🚨 THE CRITICAL INTERCEPTION:
// If N3FJP outputs its rigid 1st District placeholder, intercept it and find the real location!
if (lat === 42.4 && lon === -71.7 && call && !isClearSignal) {
const trueCoords = await fetchTrueCallCoords(call);
if (trueCoords) {
lat = trueCoords.lat;
lon = trueCoords.lon;
} else {
// If the database lookup hasn't found them yet, flag as 0,0 so the server handles it cleanly
lat = 0.0;
lon = 0.0;
}
}

const qso = {
dx_call: isClearSignal ? 'CLEAR' : call,
dx_grid: getTag('GRIDSQUARE') || getTag('GRID'),
lat: lat,
lon: lon,
status: isClearSignal ? 'clear' : eventType,
source: 'n3fjp',
ts_utc: new Date().toISOString(),
};

const payload = JSON.stringify(qso);
const req = http.request(
{
hostname: OHC_HOST,
port: OHC_PORT,
path: '/api/n3fjp/qso',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
Connection: 'close',
},
},
(res) => {
res.on('data', () => {});
},
);

req.on('error', (e) => console.error('❌ Bridge Error:', e.message));
req.write(payload);
req.end();

console.log(
`⚡ ${eventType === 'clear' ? '🗑️ CLEARED' : '📡 SENT'}: ${call || 'N/A'} (Lat: ${lat.toFixed(2)}, Lon: ${lon.toFixed(2)})`,
);
}

client.on('error', (err) => console.error('❌ Socket Error:', err.message));
client.on('close', () => {
console.log('📡 Connection to N3FJP closed. Retrying in 5s...');
setTimeout(() => client.connect(N3FJP_PORT, '127.0.0.1'), 5000);
});
14 changes: 14 additions & 0 deletions server/routes/wsjtx.js
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,20 @@ module.exports = function (app, ctx) {
locSource = 'bridge';
}

// ==========================================
// CACHE PATCH: Pull coordinates from active preview line
// ==========================================
if (!locSource && qso.status === 'log') {
const existing = n3fjpQsos.find((q) => q.dx_call === qso.dx_call && q.lat !== 0 && q.lon !== 0);
if (existing) {
qso.lat = existing.lat;
qso.lon = existing.lon;
qso.loc_source = 'cache';
locSource = 'cache';
}
}
// ==========================================

// 2) Otherwise prefer the exact operating grid (N3FJP “Grid Rec” field).
if (!locSource && qso.dx_grid) {
const loc = maidenheadToLatLon(qso.dx_grid);
Expand Down
Loading