Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5f9f43b
squash merge satellite OMM data download
MichaelWheeley May 19, 2026
e7826d9
fix(dxspider-proxy): stop quiet-band connection churn + fix auth dete…
accius May 20, 2026
78fc367
fix(dxcluster): per-fetch AbortController so HamQTH fallback can run
accius May 20, 2026
bda89ec
[satellites] update satellites tracked (#1004)
MichaelWheeley May 21, 2026
b5c1414
useLightning remove double space (#1005)
MichaelWheeley May 21, 2026
0fcdf06
Newutils cleanup (#1006)
MichaelWheeley May 21, 2026
e3dcbbc
feat(n3fjp): live entry-preview lines + preview color (#979)
accius May 21, 2026
6858893
fix(satellites) #987: hide satellite status box with the Hide UI toggle
accius May 21, 2026
0fb4947
feat(solar) #988: selectable 6/12/24/48h history for X-Ray Flux panel
accius May 21, 2026
23583d3
feat(settings) #989: user-selectable callbook for callsign lookups
accius May 21, 2026
da6a3a5
rebase to latest staging, resolve merge conflict statemachine.js
MichaelWheeley May 21, 2026
9b51241
Merge branch 'feature/sat_tle_to_omm_merge2' of github.com:MichaelWhe…
MichaelWheeley May 21, 2026
a2271f0
add CelesTrak http status code 429 as rate limit block
MichaelWheeley May 22, 2026
42b5192
- correct Axios AbortSignal timeout
MichaelWheeley May 22, 2026
2b5a7f2
- added `'use strict';`
MichaelWheeley May 22, 2026
e7cffc1
- protect `s.data_source.startsWith` with `s.data_source?.startsWith`
MichaelWheeley May 22, 2026
28e18db
- added explict messages on `try{}catch{ logWarn(...); }`
MichaelWheeley May 22, 2026
46d7cbb
- protected function by replacement of `typeof ... object` with `Arra…
MichaelWheeley May 22, 2026
db72cca
- extended `Login to Space-Track` section, now checks POST for failur…
MichaelWheeley May 22, 2026
bdbfa95
- added remarks regarding size of `ommCache` and `ommUnusedCache`
MichaelWheeley May 22, 2026
646f015
!Array.isArray
MichaelWheeley May 22, 2026
c140b16
- `'User-Agent':` switched from `'Mozilla/5.0'` to `OpenHamClock/${AP…
MichaelWheeley May 25, 2026
58925a8
- enhanced message on unknown exception
MichaelWheeley May 25, 2026
edb18cf
- enhanced message on exception in CSV reading error
MichaelWheeley May 25, 2026
01c61ff
- add kick start run() after state-machine constructor
MichaelWheeley May 25, 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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ LAYOUT=modern
# QRZ_USERNAME=your_callsign
# QRZ_PASSWORD=your_qrz_password

# Satellite data sources (effect server only)
# CelesTrak.org data source is enabled by default, no username or password is needed.
# CelesTrak can be disabled by setting CELESTRAK_ENABLED=false, but it is recommended to keep it enabled as a backup source.
CELESTRAK_ENABLED=true
# Space-Track.org is disabled by default, but can be enabled by providing a valid username and password.
# To enable both username and password must be fields must be active.
# When enabled Space-Track.org becomes primary data source for satellite data with CelesTrak as backup.
#SPACE_TRACK_USERNAME=your_username
#SPACE_TRACK_PASSWORD=your_password

# ===========================================
# FEATURE TOGGLES
# ===========================================
Expand Down
31 changes: 27 additions & 4 deletions dxspider-proxy/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ const CONFIG = {
reconnectDelayMs: 10000, // 10 seconds between reconnect attempts
maxReconnectAttempts: 3,
cleanupIntervalMs: 60000, // 1 minute
keepAliveIntervalMs: 120000, // 2 minutes - send keepalive
keepAliveIntervalMs: 60000, // 1 minute - send keepalive (must stay < socketTimeoutMs)
activityTimeoutMs: 180000, // 3 minutes - if no spots, assume dead and failover
authTimeoutMs: 30000, // 30 seconds - if no prompt after login, try next node
// Last-resort TCP backstop. Must be LONGER than activityTimeoutMs so the
// graceful node-failover watchdog acts first. A 60s value here used to
// preempt it and tear down healthy connections during quiet-band gaps.
socketTimeoutMs: 300000, // 5 minutes
};

// State
Expand Down Expand Up @@ -242,7 +246,7 @@ const connect = () => {
log('CONNECT', `Attempting connection to ${node.name} (${node.host}:${node.port})`);

client = new net.Socket();
client.setTimeout(60000); // 60 second timeout
client.setTimeout(CONFIG.socketTimeoutMs);

client.connect(node.port, node.host, () => {
connected = true;
Expand Down Expand Up @@ -313,6 +317,13 @@ const connect = () => {
if (spot) {
addSpot(spot);
resetActivityWatchdog(); // Got a spot, connection is healthy
// A flowing spot is definitive proof login succeeded. The DXSpider
// prompt has no trailing newline so it never arrives as a complete
// line — prompt-based detection alone is unreliable.
if (!authenticated) {
authenticated = true;
log('AUTH', 'Login confirmed (spot stream active)');
}
// Only reset failover counter if connection has been stable for 60s+
// A few spots before a timeout isn't truly healthy — it traps us
// on a flaky node that connects briefly then drops
Expand All @@ -328,8 +339,10 @@ const connect = () => {
continue;
}

// Detect auth completion - DX Spider sends "callsign de NODE >" prompt
if (!authenticated && /\sde\s+\S+\s*>/.test(trimmed)) {
// Detect auth completion - DX Spider sends "callsign de NODE >" prompt.
// Exclude lines containing '<' — sh/dx output (e.g. "...de Helmut<DF4IY>")
// otherwise false-matches this pattern.
if (!authenticated && !trimmed.includes('<') && /\sde\s+\S+\s*>/.test(trimmed)) {
authenticated = true;
log('AUTH', `Login confirmed: ${trimmed.substring(0, 80)}`);
resetActivityWatchdog(); // Auth done, start watching for spots
Expand All @@ -344,6 +357,16 @@ const connect = () => {
client.on('timeout', () => {
log('TIMEOUT', 'Connection timed out');
connecting = false;
// Node does NOT auto-close a socket on timeout. Without this teardown the
// old socket keeps emitting 'data' until the next connect(), spuriously
// logging "Connection stable" and resetting the failover counter.
if (client) {
try {
client.removeAllListeners();
client.destroy();
} catch (e) {}
client = null;
}
handleDisconnect();
});

Expand Down
119 changes: 105 additions & 14 deletions package-lock.json

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

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
},
"dependencies": {
"axios": "^1.6.2",
"axios-cookiejar-support": "^6.0.5",
"compression": "^1.7.4",
"convert-csv-to-json": "^4.38.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.21.2",
Expand All @@ -40,6 +42,7 @@
"react-i18next": "^16.5.4",
"rss-parser": "^3.13.0",
"satellite.js": "^6.0.0",
"tough-cookie": "^6.0.1",
"ws": "^8.14.2"
},
"devDependencies": {
Expand Down
17 changes: 17 additions & 0 deletions server/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,23 @@ const CONFIG = {
? parseFloat(process.env.DX_LONGITUDE)
: (jsonConfig.defaultDX?.lon ?? -0.1278),

// Satellites configuration
satellites: {
celestrak: {
get enabled() {
return !(process.env.CELESTRAK_ENABLED && process.env.CELESTRAK_ENABLED === 'false');
},
},
spaceTrack: {
// (do not expose usernames/passwords to frontend)
_username: process.env.SPACE_TRACK_USERNAME || '',
_password: process.env.SPACE_TRACK_PASSWORD || '',
get enabled() {
return this._username.length > 0 && this._password.length > 0;
},
},
},

// Feature toggles
showSatellites: process.env.SHOW_SATELLITES !== 'false' && jsonConfig.features?.showSatellites !== false,
showPota: process.env.SHOW_POTA !== 'false' && jsonConfig.features?.showPOTA !== false,
Expand Down
19 changes: 13 additions & 6 deletions server/routes/dxcluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -1303,8 +1303,6 @@ module.exports = function (app, ctx) {
}

try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
const now = Date.now();

// Try proxy first for better real-time data
Expand Down Expand Up @@ -1383,10 +1381,15 @@ module.exports = function (app, ctx) {

// Try proxy if not using custom or custom failed
if (newSpots.length === 0 && source !== 'custom' && source !== 'udp') {
// Each upstream gets its OWN AbortController. A shared controller meant a
// proxy timeout left the signal pre-aborted, so the HamQTH fallback below
// rejected instantly with AbortError and never actually ran.
const proxyController = new AbortController();
const proxyTimeout = setTimeout(() => proxyController.abort(), 10000);
try {
const proxyResponse = await fetch(`${DXSPIDER_PROXY_URL}/api/spots?limit=100`, {
headers: { 'User-Agent': 'OpenHamClock/3.14.11' },
signal: controller.signal,
signal: proxyController.signal,
});

if (proxyResponse.ok) {
Expand All @@ -1409,15 +1412,19 @@ module.exports = function (app, ctx) {
}
} catch (proxyErr) {
logDebug('[DX Paths] Proxy failed, trying HamQTH');
} finally {
clearTimeout(proxyTimeout);
}
}

// Fallback to HamQTH if proxy failed (never for explicit custom source)
if (newSpots.length === 0 && source !== 'custom' && source !== 'udp') {
const hamqthController = new AbortController();
const hamqthTimeout = setTimeout(() => hamqthController.abort(), 10000);
try {
const response = await fetch('https://www.hamqth.com/dxc_csv.php?limit=50', {
headers: { 'User-Agent': 'OpenHamClock/3.13.1' },
signal: controller.signal,
signal: hamqthController.signal,
});

if (response.ok) {
Expand Down Expand Up @@ -1469,11 +1476,11 @@ module.exports = function (app, ctx) {
}
} catch (hamqthErr) {
logDebug('[DX Paths] HamQTH also failed');
} finally {
clearTimeout(hamqthTimeout);
}
}

clearTimeout(timeout);

if (newSpots.length === 0) {
// Return existing paths if fetch failed
const validPaths = pathsCache.allPaths.filter((p) => now - p.timestamp < DXPATHS_RETENTION);
Expand Down
Loading
Loading