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.
openhamclock/server.js

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);
});

Powered by TurnKey Linux.