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.
236 lines
8.5 KiB
236 lines
8.5 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 (for future WebSocket implementation)
|
|
app.get('/api/dxcluster/spots', async (req, res) => {
|
|
try {
|
|
// Try DXWatch first
|
|
const response = await fetch('https://www.dxwatch.com/api/spots.json?limit=20', {
|
|
headers: { 'User-Agent': 'OpenHamClock/3.0' }
|
|
});
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
res.json(data);
|
|
} else {
|
|
// Return empty array if API unavailable
|
|
res.json([]);
|
|
}
|
|
} catch (error) {
|
|
console.error('DX Cluster API error:', error.message);
|
|
res.json([]); // Return empty array on error
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
});
|