Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion 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 Expand Up @@ -40,12 +41,12 @@
"mqtt": "^5.15.0",
"node-fetch": "^2.7.0",
"papaparse": "^5.5.3",
"prom-client": "^15.1.3",
"react-colorful": "^5.6.1",
"react-i18next": "^16.5.4",
"rss-parser": "^3.13.0",
"satellite.js": "^6.0.0",
"tough-cookie": "^6.0.1",
"prom-client": "^15.1.0",
"ws": "^8.14.2"
},
"devDependencies": {
Expand Down
255 changes: 255 additions & 0 deletions scripts/n3fjp-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
const net = require('net');
const http = require('http');

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

// N3FJP default placeholder coordinates for the 1st call district.
const N3FJP_DEFAULT_LAT = 42.4;
const N3FJP_DEFAULT_LON = -71.7;

let N3FJP_HOST = process.env.N3FJP_TARGET_HOST || '127.0.0.1';
let N3FJP_PORT = parseInt(process.env.N3FJP_TARGET_PORT, 10) || 1100;

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

// 💡 Fetch configuration dynamically from the database via your server's API
function initBridge() {
http
.get(`http://${OHC_HOST}:${OHC_PORT}/api/settings`, (res) => {
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
try {
const settings = JSON.parse(body);
// Fallback checks for whatever key you named your integration fields
if (settings.n3fjp_host || settings.n3fjpIp) {
N3FJP_HOST = settings.n3fjp_host || settings.n3fjpIp;
N3FJP_PORT = parseInt(settings.n3fjp_port || settings.n3fjpPort, 10) || 1100;
}
} catch (e) {
console.log('ℹ️ Could not parse settings API response, using defaults.');
}

console.log(`📡 Attempting network connection to N3FJP at ${N3FJP_HOST}:${N3FJP_PORT}...`);
client.connect(N3FJP_PORT, N3FJP_HOST, () => {
console.log(`✅ Bridge Connected to N3FJP at ${N3FJP_HOST}:${N3FJP_PORT} (Low-Latency Mode)`);
reportConnectionStatus(true);
});
});
})
.on('error', () => {
console.log('⚠️ Settings API unavailable yet. Connecting to local default...');
client.connect(N3FJP_PORT, N3FJP_HOST, () => {
reportConnectionStatus(true);
});
});
}

// Helper to broadcast the TCP socket status back to the main server
function reportConnectionStatus(isConnected) {
const payload = JSON.stringify({ source: 'n3fjp', connected: isConnected });

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

req.on('error', () => {
/* Suppress error if main server is restarting */
});
req.write(payload);
req.end();
}

// Start the bridge initialization
initBridge();

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;
}

// 💡 BOUNDS CHECK (Per K0CJH feedback): Ensure coordinates fall within valid ranges
if (isLongitude) {
if (decimal < -180 || decimal > 180) return 0;
} else {
if (decimal < -90 || decimal > 90) return 0;
}

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();
});
}

// 💡 THE PREVIEW CACHE: Tracks coordinates between tabbing out and logging the contact
const activePreviews = {};

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;

// Handle clearing out the cache if you clear the contact window in N3FJP
if (isClearSignal && call) {
delete activePreviews[call];
}

// 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);

const isPlaceholder = lat === N3FJP_DEFAULT_LAT && lon === N3FJP_DEFAULT_LON;
const isBlankCoords = lat === 0.0 && lon === 0.0;

// 📡 PREVIEW PHASE: Capture the accurate coordinates when you tab out
if (eventType === 'preview') {
if ((isPlaceholder || isBlankCoords) && call) {
const trueCoords = await fetchTrueCallCoords(call);
if (trueCoords) {
lat = trueCoords.lat;
lon = trueCoords.lon;
} else {
lat = undefined;
lon = undefined;
}
}
if (call) {
activePreviews[call] = { lat, lon };
}
}

// 📝 LOGGING PHASE: Lock in the preview position to defeat N3FJP's call-district overrides
if (eventType === 'log' && call) {
if (activePreviews[call]) {
lat = activePreviews[call].lat;
lon = activePreviews[call].lon;
delete activePreviews[call]; // Clean up memory
} else if (isPlaceholder || isBlankCoords) {
lat = undefined;
lon = undefined;
}
}

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' : eventType === 'preview' ? '📡 PREVIEW' : '💾 LOGGED'}: ${call || 'N/A'} (Lat: ${lat ? lat.toFixed(2) : 'AUTO'}, Lon: ${lon ? lon.toFixed(2) : 'AUTO'})`,
);
}

// Global listeners for handling drops and automatic retries
client.on('error', (err) => {
console.error('❌ Socket Error:', err.message);
reportConnectionStatus(false);
});

client.on('close', () => {
console.log(`📡 Connection to N3FJP closed. Retrying to connect to ${N3FJP_HOST} in 5s...`);
reportConnectionStatus(false);
setTimeout(() => client.connect(N3FJP_PORT, N3FJP_HOST), 5000);
});
Loading
Loading