diff --git a/public/index.html b/public/index.html index 538a8ba..8d20073 100644 --- a/public/index.html +++ b/public/index.html @@ -2596,16 +2596,18 @@ > + + -

{dxClusterSource === 'auto' && '→ Automatically selects the best available source'} - {dxClusterSource === 'hamqth' && '→ HamQTH.com DX Cluster feed'} + {dxClusterSource === 'hamqth' && '→ HamQTH.com DX Cluster CSV feed'} + {dxClusterSource === 'iu1bow' && '→ IU1BOW.it Spiderweb cluster (HTTP API)'} + {dxClusterSource === 'spothole' && '→ Spothole.app aggregated DX cluster'} {dxClusterSource === 'dxheat' && '→ DXHeat.com real-time cluster'} {dxClusterSource === 'dxsummit' && '→ DXSummit.fi cluster (may be slow)'} - {dxClusterSource === 'dxspider' && '→ G6NHU-2 DX Spider node (dxspider.co.uk)'}

diff --git a/server.js b/server.js index b6c8d09..6815caf 100644 --- a/server.js +++ b/server.js @@ -15,7 +15,6 @@ const express = require('express'); const cors = require('cors'); const path = require('path'); const fetch = require('node-fetch'); -const net = require('net'); const app = express(); const PORT = process.env.PORT || 3000; @@ -117,11 +116,10 @@ app.get('/api/hamqsl/conditions', async (req, res) => { }); // DX Cluster proxy - fetches from selectable sources -// Query param: ?source=hamqth|dxheat|dxsummit|dxspider|auto (default: auto) +// Query param: ?source=hamqth|dxheat|dxsummit|jo30|auto (default: auto) -// Cache for DX Spider telnet spots (to avoid too many connections) -let dxSpiderCache = { spots: [], timestamp: 0 }; -const DXSPIDER_CACHE_TTL = 60000; // 60 seconds cache +// Note: DX Spider (telnet) removed - doesn't work on hosted platforms +// Using HTTP-based APIs only for online compatibility app.get('/api/dxcluster/spots', async (req, res) => { const source = (req.query.source || 'auto').toLowerCase(); @@ -274,117 +272,111 @@ app.get('/api/dxcluster/spots', async (req, res) => { return null; } - // Helper function for DX Spider (G6NHU-2) via telnet - async function fetchDXSpider() { - // Check cache first - if (Date.now() - dxSpiderCache.timestamp < DXSPIDER_CACHE_TTL && dxSpiderCache.spots.length > 0) { - console.log('[DX Cluster] DX Spider: returning', dxSpiderCache.spots.length, 'cached spots'); - return dxSpiderCache.spots; - } + // Helper function for IU1BOW Spiderweb (HTTP-based DX Spider web interface) + async function fetchIU1BOW() { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 8000); - return new Promise((resolve) => { - const spots = []; - let buffer = ''; - let gotSpots = false; - let loginSent = false; - let commandSent = false; - - const client = new net.Socket(); - client.setTimeout(12000); - - client.connect(7300, 'dxspider.co.uk', () => { - console.log('[DX Cluster] DX Spider: connected'); + try { + const response = await fetch('https://www.iu1bow.it/spotlist', { + headers: { + 'User-Agent': 'OpenHamClock/3.4', + 'Accept': 'application/json' + }, + signal: controller.signal }); + clearTimeout(timeout); - client.on('data', (data) => { - buffer += data.toString(); - - // Wait for login prompt - if (!loginSent && (buffer.includes('login:') || buffer.includes('Please enter your call'))) { - loginSent = true; - // Send a guest login (GUEST or anonymous callsign) - client.write('GUEST\r\n'); - return; - } - - // Wait for prompt after login, then send command - if (loginSent && !commandSent && (buffer.includes('Hello') || buffer.includes('de ') || buffer.includes('>'))) { - commandSent = true; - // Request last 25 spots - setTimeout(() => { - client.write('sh/dx 25\r\n'); - }, 500); - return; - } + if (response.ok) { + const data = await response.json(); - // Parse DX spots from the output - // Format: DX de W3LPL: 14195.0 TI5/AA8HH FT8 -09 dB 1234Z - const lines = buffer.split('\n'); - for (const line of lines) { - if (line.includes('DX de ')) { - const match = line.match(/DX de ([A-Z0-9\/\-]+):\s+(\d+\.?\d*)\s+([A-Z0-9\/\-]+)\s+(.+?)\s+(\d{4})Z/i); - if (match) { - const spotter = match[1].replace(':', ''); - const freqKhz = parseFloat(match[2]); - const dxCall = match[3]; - const comment = match[4].trim(); - const timeStr = match[5]; - - if (!isNaN(freqKhz) && freqKhz > 0 && dxCall) { - const freqMhz = (freqKhz / 1000).toFixed(3); - const time = timeStr.substring(0, 2) + ':' + timeStr.substring(2, 4) + 'z'; - - // Avoid duplicates - if (!spots.find(s => s.call === dxCall && s.freq === freqMhz)) { - spots.push({ - freq: freqMhz, - call: dxCall, - comment: comment, - time: time, - spotter: spotter, - source: 'DX Spider' - }); - gotSpots = true; - } + if (Array.isArray(data) && data.length > 0) { + const spots = data.slice(0, 25).map(spot => { + // IU1BOW format varies, common fields: freq, spotcall/dx_call, spotter, time, comment + const freqVal = spot.freq || spot.frequency || 0; + const freqMhz = freqVal > 1000 ? (freqVal / 1000).toFixed(3) : String(freqVal).includes('.') ? String(freqVal) : (freqVal / 1000).toFixed(3); + let time = ''; + if (spot.time) { + // Time might be Unix timestamp or string + if (typeof spot.time === 'number') { + const d = new Date(spot.time * 1000); + time = d.toISOString().substring(11, 16) + 'z'; + } else { + time = String(spot.time).substring(0, 5) + 'z'; } } - } - } - - // If we have enough spots or see end marker, close connection - if (spots.length >= 20 || buffer.includes('G6NHU-2 de GUEST')) { - client.write('bye\r\n'); - setTimeout(() => client.destroy(), 500); - } - }); - - client.on('timeout', () => { - console.log('[DX Cluster] DX Spider: timeout'); - client.destroy(); - }); - - client.on('error', (err) => { - console.error('[DX Cluster] DX Spider error:', err.message); - client.destroy(); - }); - - client.on('close', () => { - if (spots.length > 0) { - console.log('[DX Cluster] DX Spider:', spots.length, 'spots'); - dxSpiderCache = { spots: spots, timestamp: Date.now() }; - resolve(spots); - } else { - resolve(null); + return { + freq: freqMhz, + call: spot.spotcall || spot.dx_call || spot.dx || 'UNKNOWN', + comment: spot.comment || spot.info || '', + time: time, + spotter: spot.spotter || spot.de || '', + source: 'IU1BOW DX Spider' + }; + }); + console.log('[DX Cluster] IU1BOW:', spots.length, 'spots'); + return spots; } + } + } catch (error) { + clearTimeout(timeout); + if (error.name !== 'AbortError') { + console.error('[DX Cluster] IU1BOW error:', error.message); + } + } + return null; + } + + // Helper function for Spothole (aggregated DX cluster + xOTA spots) + async function fetchSpothole() { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 8000); + + try { + // Spothole API endpoint - filter for DX cluster spots only + const response = await fetch('https://spothole.app/api/spots?sources=dxcluster&limit=25', { + headers: { + 'User-Agent': 'OpenHamClock/3.4', + 'Accept': 'application/json' + }, + signal: controller.signal }); + clearTimeout(timeout); - // Fallback timeout - setTimeout(() => { - if (!gotSpots) { - client.destroy(); + if (response.ok) { + const data = await response.json(); + const spotsList = data.spots || data; + + if (Array.isArray(spotsList) && spotsList.length > 0) { + const spots = spotsList.slice(0, 25).map(spot => { + // Spothole format: dx, frequency, mode, comment, de, time, etc. + const freqVal = spot.frequency || spot.freq || 0; + const freqMhz = freqVal > 1000 ? (freqVal / 1000).toFixed(3) : String(freqVal); + let time = ''; + if (spot.time || spot.timestamp) { + const d = new Date(spot.time || spot.timestamp); + time = d.toISOString().substring(11, 16) + 'z'; + } + return { + freq: freqMhz, + call: spot.dx || spot.call || spot.spotted || 'UNKNOWN', + comment: spot.comment || spot.info || spot.mode || '', + time: time, + spotter: spot.de || spot.spotter || '', + source: 'Spothole' + }; + }); + console.log('[DX Cluster] Spothole:', spots.length, 'spots'); + return spots; } - }, 15000); - }); + } + } catch (error) { + clearTimeout(timeout); + if (error.name !== 'AbortError') { + console.error('[DX Cluster] Spothole error:', error.message); + } + } + return null; } // Fetch based on selected source @@ -396,11 +388,15 @@ app.get('/api/dxcluster/spots', async (req, res) => { spots = await fetchDXHeat(); } else if (source === 'dxsummit') { spots = await fetchDXSummit(); - } else if (source === 'dxspider') { - spots = await fetchDXSpider(); + } else if (source === 'iu1bow') { + spots = await fetchIU1BOW(); + } else if (source === 'spothole') { + spots = await fetchSpothole(); } else { - // Auto mode - try sources in order + // Auto mode - try sources in order (most reliable first) spots = await fetchHamQTH(); + if (!spots) spots = await fetchIU1BOW(); + if (!spots) spots = await fetchSpothole(); if (!spots) spots = await fetchDXHeat(); if (!spots) spots = await fetchDXSummit(); } @@ -412,10 +408,11 @@ app.get('/api/dxcluster/spots', async (req, res) => { app.get('/api/dxcluster/sources', (req, res) => { res.json([ { id: 'auto', name: 'Auto (Best Available)', description: 'Automatically selects the best available source' }, - { id: 'hamqth', name: 'HamQTH', description: 'HamQTH.com DX Cluster feed' }, + { id: 'hamqth', name: 'HamQTH', description: 'HamQTH.com DX Cluster CSV feed' }, + { id: 'iu1bow', name: 'IU1BOW DX Spider', description: 'IU1BOW.it Spiderweb cluster (HTTP API)' }, + { id: 'spothole', name: 'Spothole', description: 'Spothole.app aggregated DX cluster' }, { id: 'dxheat', name: 'DXHeat', description: 'DXHeat.com real-time cluster' }, - { id: 'dxsummit', name: 'DX Summit', description: 'DXSummit.fi cluster (may be slow)' }, - { id: 'dxspider', name: 'DX Spider (G6NHU)', description: 'G6NHU-2 DX Spider node via telnet (dxspider.co.uk)' } + { id: 'dxsummit', name: 'DX Summit', description: 'DXSummit.fi cluster (may be slow)' } ]); });