You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
307 lines
11 KiB
307 lines
11 KiB
/**
|
|
* 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);
|
|
});
|