/** * OpenHamClock Server * * Express server that: * 1. Serves the static web application * 2. Proxies API requests to avoid CORS issues * 3. Provides WebSocket support for future real-time features * * Usage: * node server.js * PORT=8080 node server.js */ const express = require('express'); const cors = require('cors'); const path = require('path'); const fetch = require('node-fetch'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); // Serve static files from public directory app.use(express.static(path.join(__dirname, 'public'))); // ============================================ // API PROXY ENDPOINTS // ============================================ // NOAA Space Weather - Solar Flux app.get('/api/noaa/flux', async (req, res) => { try { const response = await fetch('https://services.swpc.noaa.gov/json/f107_cm_flux.json'); const data = await response.json(); res.json(data); } catch (error) { console.error('NOAA Flux API error:', error.message); res.status(500).json({ error: 'Failed to fetch solar flux data' }); } }); // NOAA Space Weather - K-Index app.get('/api/noaa/kindex', async (req, res) => { try { const response = await fetch('https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json'); const data = await response.json(); res.json(data); } catch (error) { console.error('NOAA K-Index API error:', error.message); res.status(500).json({ error: 'Failed to fetch K-index data' }); } }); // NOAA Space Weather - Sunspots app.get('/api/noaa/sunspots', async (req, res) => { try { const response = await fetch('https://services.swpc.noaa.gov/json/solar-cycle/observed-solar-cycle-indices.json'); const data = await response.json(); res.json(data); } catch (error) { console.error('NOAA Sunspots API error:', error.message); res.status(500).json({ error: 'Failed to fetch sunspot data' }); } }); // NOAA Space Weather - X-Ray Flux app.get('/api/noaa/xray', async (req, res) => { try { const response = await fetch('https://services.swpc.noaa.gov/json/goes/primary/xrays-7-day.json'); const data = await response.json(); res.json(data); } catch (error) { console.error('NOAA X-Ray API error:', error.message); res.status(500).json({ error: 'Failed to fetch X-ray data' }); } }); // POTA Spots app.get('/api/pota/spots', async (req, res) => { try { const response = await fetch('https://api.pota.app/spot/activator'); const data = await response.json(); res.json(data); } catch (error) { console.error('POTA API error:', error.message); res.status(500).json({ error: 'Failed to fetch POTA spots' }); } }); // SOTA Spots app.get('/api/sota/spots', async (req, res) => { try { const response = await fetch('https://api2.sota.org.uk/api/spots/50/all'); const data = await response.json(); res.json(data); } catch (error) { console.error('SOTA API error:', error.message); res.status(500).json({ error: 'Failed to fetch SOTA spots' }); } }); // HamQSL Band Conditions app.get('/api/hamqsl/conditions', async (req, res) => { try { const response = await fetch('https://www.hamqsl.com/solarxml.php'); const text = await response.text(); res.set('Content-Type', 'application/xml'); res.send(text); } catch (error) { console.error('HamQSL API error:', error.message); res.status(500).json({ error: 'Failed to fetch band conditions' }); } }); // DX Cluster proxy - fetches from multiple sources app.get('/api/dxcluster/spots', async (req, res) => { try { // Try DXWatch first const response = await fetch('https://dxwatch.com/dxsd1/s.php?s=0&r=50&cdx=', { headers: { 'User-Agent': 'OpenHamClock/3.0', 'Accept': 'application/json' }, timeout: 5000 }); if (response.ok) { const text = await response.text(); try { // DXWatch returns JSON array const data = JSON.parse(text); const spots = data.map(spot => ({ freq: spot.fr ? (parseFloat(spot.fr) / 1000).toFixed(3) : spot.frequency, call: spot.dx || spot.dx_call, comment: spot.cm || spot.comment || '', time: spot.t || spot.time || '', spotter: spot.sp || spot.spotter })).slice(0, 20); return res.json(spots); } catch (parseErr) { console.log('DXWatch parse error, trying alternate format'); } } } catch (error) { console.error('DXWatch API error:', error.message); } // Try HamQTH DX Cluster as fallback try { const response = await fetch('https://www.hamqth.com/dxc_csv.php?limit=25', { headers: { 'User-Agent': 'OpenHamClock/3.0' }, timeout: 5000 }); if (response.ok) { const text = await response.text(); const lines = text.trim().split('\n'); const spots = lines.slice(0, 20).map(line => { const parts = line.split(','); return { freq: parts[1] ? (parseFloat(parts[1]) / 1000).toFixed(3) : '0.000', call: parts[2] || 'UNKNOWN', comment: parts[5] || '', time: parts[4] ? parts[4].substring(0, 5) + 'z' : '', spotter: parts[3] || '' }; }).filter(s => s.call !== 'UNKNOWN'); if (spots.length > 0) { return res.json(spots); } } } catch (error) { console.error('HamQTH DX Cluster error:', error.message); } // Try DX Summit RSS as another fallback try { const response = await fetch('https://www.dxsummit.fi/api/v1/spots?limit=25', { headers: { 'User-Agent': 'OpenHamClock/3.0' }, timeout: 5000 }); if (response.ok) { const data = await response.json(); const spots = data.map(spot => ({ freq: spot.frequency ? (parseFloat(spot.frequency) / 1000).toFixed(3) : '0.000', call: spot.dx_call || spot.callsign, comment: spot.info || spot.comment || '', time: spot.time ? spot.time.substring(11, 16) + 'z' : '', spotter: spot.spotter || '' })).slice(0, 20); if (spots.length > 0) { return res.json(spots); } } } catch (error) { console.error('DX Summit API error:', error.message); } // Return empty array if all sources fail res.json([]); }); // QRZ Callsign lookup (requires API key) app.get('/api/qrz/lookup/:callsign', async (req, res) => { const { callsign } = req.params; // Note: QRZ requires an API key - this is a placeholder res.json({ message: 'QRZ lookup requires API key configuration', callsign: callsign.toUpperCase() }); }); // ============================================ // HEALTH CHECK // ============================================ app.get('/api/health', (req, res) => { res.json({ status: 'ok', version: '3.0.0', uptime: process.uptime(), timestamp: new Date().toISOString() }); }); // ============================================ // CONFIGURATION ENDPOINT // ============================================ app.get('/api/config', (req, res) => { res.json({ version: '3.0.0', features: { spaceWeather: true, pota: true, sota: true, dxCluster: true, satellites: false, // Coming soon contests: false // Coming soon }, refreshIntervals: { spaceWeather: 300000, pota: 60000, sota: 60000, dxCluster: 30000 } }); }); // ============================================ // CATCH-ALL FOR SPA // ============================================ app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // ============================================ // START SERVER // ============================================ app.listen(PORT, () => { console.log(''); console.log('╔═══════════════════════════════════════════════════════╗'); console.log('║ ║'); console.log('║ ██████╗ ██████╗ ███████╗███╗ ██╗ ║'); console.log('║ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ║'); console.log('║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ║'); console.log('║ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ║'); console.log('║ ╚██████╔╝██║ ███████╗██║ ╚████║ ║'); console.log('║ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ║'); console.log('║ ║'); console.log('║ ██╗ ██╗ █████╗ ███╗ ███╗ ██████╗██╗ ██╗ ██╗ ║'); console.log('║ ██║ ██║██╔══██╗████╗ ████║██╔════╝██║ ██║ ██╔╝ ║'); console.log('║ ███████║███████║██╔████╔██║██║ ██║ █████╔╝ ║'); console.log('║ ██╔══██║██╔══██║██║╚██╔╝██║██║ ██║ ██╔═██╗ ║'); console.log('║ ██║ ██║██║ ██║██║ ╚═╝ ██║╚██████╗███████╗██║ ██╗ ║'); console.log('║ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ║'); console.log('║ ║'); console.log('╚═══════════════════════════════════════════════════════╝'); console.log(''); console.log(` 🌐 Server running at http://localhost:${PORT}`); console.log(' 📡 API proxy enabled for NOAA, POTA, SOTA, DX Cluster'); console.log(' 🖥️ Open your browser to start using OpenHamClock'); console.log(''); console.log(' In memory of Elwood Downey, WB0OEW'); console.log(' 73 de OpenHamClock contributors'); console.log(''); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { console.log('\nShutting down...'); process.exit(0); });