Skip to content

Commit 69f7111

Browse files
authored
Merge pull request #715 from accius/Staging
Staging
2 parents 5aa90a6 + d42d7d4 commit 69f7111

3 files changed

Lines changed: 13111 additions & 554 deletions

File tree

Dockerfile

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,6 @@ ENV NODE_ENV=production
3636
ENV PORT=3000
3737
ENV NODE_OPTIONS="--max-old-space-size=2048 --expose-gc"
3838

39-
# Create non-root user for running the application
40-
RUN addgroup -g 1001 -S nodejs && \
41-
adduser -S nodejs -u 1001 -G nodejs
42-
4339
WORKDIR /app
4440

4541
# Create /data directory for persistent stats (Railway volume mount point)
@@ -70,12 +66,6 @@ COPY --from=builder /app/public ./public
7066
# Create local data directory as fallback
7167
RUN mkdir -p /app/data
7268

73-
# Set ownership so non-root user can write to data directories and .git (auto-update)
74-
RUN chown -R nodejs:nodejs /app /data
75-
76-
# Run as non-root user
77-
USER nodejs
78-
7969
# Expose ports (3000 = web, 2237 = WSJT-X UDP, 12060 = N1MM/DXLog)
8070
EXPOSE 3000
8171
EXPOSE 2237/udp

dxspider-proxy/server.js

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,35 +19,40 @@ app.use(express.json());
1919
const CONFIG = {
2020
// DX Spider nodes to try (in order)
2121
nodes: [
22-
{ host: 'dxspider.co.uk', port: 7300, name: 'DX Spider UK (G6NHU)' },
2322
{ host: 'dxc.nc7j.com', port: 7373, name: 'NC7J' },
2423
{ host: 'dxc.ai9t.com', port: 7373, name: 'AI9T' },
2524
{ host: 'dxc.w6cua.org', port: 7300, name: 'W6CUA' },
25+
{ host: 'dxspider.co.uk', port: 7300, name: 'DX Spider UK (G6NHU)' },
2626
],
2727
// Callsign with SSID - use env var as-is, or default to OPENHAMCLOCK-56
2828
// Set CALLSIGN=YOURCALL-56 for production, CALLSIGN=YOURCALL-57 for staging
29-
callsign: process.env.CALLSIGN || 'OPENHAMCLOCK-56',
29+
callsign: process.env.CALLSIGN?.trim() || 'OPENHAMCLOCK-56',
3030
spotRetentionMs: 30 * 60 * 1000, // 30 minutes
3131
reconnectDelayMs: 10000, // 10 seconds between reconnect attempts
3232
maxReconnectAttempts: 3,
3333
cleanupIntervalMs: 60000, // 1 minute
3434
keepAliveIntervalMs: 120000, // 2 minutes - send keepalive
35+
activityTimeoutMs: 180000, // 3 minutes - if no spots, assume dead and failover
36+
authTimeoutMs: 30000, // 30 seconds - if no prompt after login, try next node
3537
};
3638

3739
// State
3840
let spots = [];
3941
let client = null;
4042
let connected = false;
41-
let connecting = false; // NEW: Prevent concurrent connection attempts
43+
let connecting = false; // Prevent concurrent connection attempts
44+
let authenticated = false; // Track whether login completed
4245
let currentNode = null;
4346
let currentNodeIndex = 0;
4447
let reconnectAttempts = 0;
4548
let lastSpotTime = null;
49+
let lastDataTime = null; // Track ANY data received from node
4650
let totalSpotsReceived = 0;
4751
let connectionStartTime = null;
4852
let buffer = '';
4953
let reconnectTimer = null;
5054
let keepAliveTimer = null;
55+
let activityWatchdog = null; // Fires if no spots arrive within threshold
5156

5257
// Logging helper with log levels
5358
// LOG_LEVEL: 'debug' = verbose, 'info' = normal, 'warn' = warnings+errors only
@@ -60,12 +65,14 @@ const CATEGORY_LEVELS = {
6065
SPOT: 'debug', // Per-spot logging is debug-only
6166
CLEANUP: 'debug', // Periodic cleanup is debug-only
6267
KEEPALIVE: 'debug', // Keepalive pings are debug-only
68+
DATA: 'debug', // Non-spot telnet data is debug-only
6369
CMD: 'debug', // Command logging is debug-only
6470
AUTH: 'info', // Auth events are informational
6571
CONNECT: 'info', // Connection events are informational
6672
CLOSE: 'info',
6773
RECONNECT: 'info',
6874
FAILOVER: 'info',
75+
ACTIVITY: 'info',
6976
API: 'info',
7077
START: 'info',
7178
CONFIG: 'info',
@@ -224,6 +231,12 @@ const connect = () => {
224231
client = null;
225232
}
226233

234+
// Clear any stale watchdog from previous connection
235+
if (activityWatchdog) {
236+
clearTimeout(activityWatchdog);
237+
activityWatchdog = null;
238+
}
239+
227240
const node = CONFIG.nodes[currentNodeIndex];
228241
currentNode = node;
229242

@@ -235,9 +248,12 @@ const connect = () => {
235248
client.connect(node.port, node.host, () => {
236249
connected = true;
237250
connecting = false;
238-
reconnectAttempts = 0;
251+
authenticated = false;
239252
connectionStartTime = new Date();
253+
lastDataTime = Date.now();
240254
buffer = '';
255+
// NOTE: reconnectAttempts is NOT reset here — only when spots actually arrive.
256+
// This prevents infinite loops on nodes that accept TCP but kick after auth.
241257
log('CONNECT', `Connected to ${node.name}`);
242258

243259
// Send login after short delay
@@ -258,10 +274,21 @@ const connect = () => {
258274
if (client && connected) {
259275
client.write('set/dx\r\n');
260276
log('CMD', 'Sent: set/dx (enable spot stream)');
277+
278+
// Start the activity watchdog now that commands are sent
279+
// If no spots arrive within activityTimeoutMs, we'll failover
280+
resetActivityWatchdog();
261281
}
262282
}, 2000);
263283
}
264284
}, 2000);
285+
286+
// Auth timeout: if node doesn't respond with a prompt, log a warning
287+
setTimeout(() => {
288+
if (connected && !authenticated) {
289+
log('AUTH', `No auth confirmation within ${CONFIG.authTimeoutMs / 1000}s — node may be unresponsive`);
290+
}
291+
}, CONFIG.authTimeoutMs);
265292
}
266293
}, 1000);
267294

@@ -271,6 +298,7 @@ const connect = () => {
271298

272299
client.on('data', (data) => {
273300
buffer += data.toString();
301+
lastDataTime = Date.now();
274302

275303
// Process complete lines
276304
const lines = buffer.split('\n');
@@ -285,8 +313,26 @@ const connect = () => {
285313
const spot = parseSpotLine(trimmed);
286314
if (spot) {
287315
addSpot(spot);
316+
resetActivityWatchdog(); // Got a spot, connection is healthy
317+
// Connection proved healthy — reset the failover counter
318+
if (reconnectAttempts > 0) {
319+
log('CONNECT', `Connection healthy (spots flowing), resetting failover counter`);
320+
reconnectAttempts = 0;
321+
}
288322
}
323+
continue;
324+
}
325+
326+
// Detect auth completion - DX Spider sends "callsign de NODE >" prompt
327+
if (!authenticated && /\sde\s+\S+\s*>/.test(trimmed)) {
328+
authenticated = true;
329+
log('AUTH', `Login confirmed: ${trimmed.substring(0, 80)}`);
330+
resetActivityWatchdog(); // Auth done, start watching for spots
331+
continue;
289332
}
333+
334+
// Log non-spot data so we can diagnose issues (debug level)
335+
log('DATA', trimmed.substring(0, 120));
290336
}
291337
});
292338

@@ -311,6 +357,48 @@ const connect = () => {
311357
});
312358
};
313359

360+
// Reset the activity watchdog - called when spots arrive
361+
const resetActivityWatchdog = () => {
362+
if (activityWatchdog) {
363+
clearTimeout(activityWatchdog);
364+
}
365+
366+
activityWatchdog = setTimeout(() => {
367+
if (connected) {
368+
log('ACTIVITY', `No spots received in ${CONFIG.activityTimeoutMs / 1000}s — forcing failover`);
369+
// Skip straight to next node instead of retrying the same one
370+
currentNodeIndex = (currentNodeIndex + 1) % CONFIG.nodes.length;
371+
reconnectAttempts = 0;
372+
log('FAILOVER', `Switching to node: ${CONFIG.nodes[currentNodeIndex].name}`);
373+
374+
// Force disconnect and reconnect
375+
if (client) {
376+
try {
377+
client.removeAllListeners();
378+
client.destroy();
379+
} catch (e) {}
380+
client = null;
381+
}
382+
connected = false;
383+
connecting = false;
384+
authenticated = false;
385+
386+
if (keepAliveTimer) {
387+
clearInterval(keepAliveTimer);
388+
keepAliveTimer = null;
389+
}
390+
391+
// Clear any pending reconnect and connect immediately
392+
if (reconnectTimer) {
393+
clearTimeout(reconnectTimer);
394+
reconnectTimer = null;
395+
}
396+
397+
connect();
398+
}
399+
}, CONFIG.activityTimeoutMs);
400+
};
401+
314402
// Start keepalive timer
315403
const startKeepAlive = () => {
316404
if (keepAliveTimer) {
@@ -339,24 +427,36 @@ const handleDisconnect = () => {
339427

340428
connected = false;
341429
connecting = false;
430+
authenticated = false;
342431

343432
if (keepAliveTimer) {
344433
clearInterval(keepAliveTimer);
345434
keepAliveTimer = null;
346435
}
347436

437+
if (activityWatchdog) {
438+
clearTimeout(activityWatchdog);
439+
activityWatchdog = null;
440+
}
441+
348442
// Don't schedule another reconnect if one is already pending
349443
if (reconnectTimer) {
350444
return;
351445
}
352446

447+
// Detect rapid disconnect (kicked within seconds of connecting)
448+
const connectionDuration = connectionStartTime ? Date.now() - connectionStartTime.getTime() : 0;
449+
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)`);
451+
}
452+
353453
reconnectAttempts++;
354454

355455
if (reconnectAttempts >= CONFIG.maxReconnectAttempts) {
356456
// Try next node
357457
currentNodeIndex = (currentNodeIndex + 1) % CONFIG.nodes.length;
358458
reconnectAttempts = 0;
359-
log('FAILOVER', `Switching to node: ${CONFIG.nodes[currentNodeIndex].name}`);
459+
log('FAILOVER', `${CONFIG.maxReconnectAttempts} consecutive failures — switching to node: ${CONFIG.nodes[currentNodeIndex].name}`);
360460
}
361461

362462
log('RECONNECT', `Attempting reconnect in ${CONFIG.reconnectDelayMs}ms (attempt ${reconnectAttempts})`);
@@ -376,10 +476,12 @@ app.get('/health', (req, res) => {
376476
res.json({
377477
status: 'ok',
378478
connected,
479+
authenticated,
379480
currentNode: currentNode?.name || 'none',
380481
spotsInMemory: spots.length,
381482
totalSpotsReceived,
382483
lastSpotTime: lastSpotTime?.toISOString() || null,
484+
lastDataTime: lastDataTime ? new Date(lastDataTime).toISOString() : null,
383485
connectionUptime: connectionStartTime
384486
? Math.floor((Date.now() - connectionStartTime.getTime()) / 1000) + 's'
385487
: null,
@@ -544,6 +646,7 @@ setInterval(cleanupSpots, CONFIG.cleanupIntervalMs);
544646
app.listen(PORT, () => {
545647
log('START', `DX Spider Proxy listening on port ${PORT}`);
546648
log('CONFIG', `Callsign: ${CONFIG.callsign}`);
649+
log('CONFIG', `CALLSIGN env var: ${process.env.CALLSIGN === undefined ? '(not set)' : `"${process.env.CALLSIGN}"`}`);
547650
log('CONFIG', `Spot retention: ${CONFIG.spotRetentionMs / 60000} minutes`);
548651
log('CONFIG', `Available nodes: ${CONFIG.nodes.map((n) => n.name).join(', ')}`);
549652

0 commit comments

Comments
 (0)