Merge pull request #20 from accius/Modular-Staging

Modular staging
pull/36/head
accius 3 days ago committed by GitHub
commit 78469d4ba2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -16,8 +16,8 @@ CALLSIGN=N0CALL
LOCATOR=FN31 LOCATOR=FN31
# Your station coordinates (optional - calculated from LOCATOR if not set) # Your station coordinates (optional - calculated from LOCATOR if not set)
LATITUDE= # LATITUDE=40.7128
LONGITUDE= # LONGITUDE=-74.0060
# =========================================== # ===========================================
# SERVER SETTINGS # SERVER SETTINGS
@ -53,14 +53,16 @@ LAYOUT=modern
# =========================================== # ===========================================
# ITURHFProp service URL (for advanced propagation predictions) # ITURHFProp service URL (for advanced propagation predictions)
ITURHFPROP_URL= # Only uncomment if you have your own ITURHFProp service running
# ITURHFPROP_URL=https://your-iturhfprop-service.com
# DX Spider Proxy URL (for DX cluster spots) # DX Spider Proxy URL (for DX cluster spots)
DXSPIDER_PROXY_URL= # Only uncomment if you have your own proxy running
# DXSPIDER_PROXY_URL=https://your-dxspider-proxy.com
# OpenWeatherMap API key (for local weather display) # OpenWeatherMap API key (for local weather display)
# Get a free key at https://openweathermap.org/api # Get a free key at https://openweathermap.org/api
OPENWEATHER_API_KEY= # OPENWEATHER_API_KEY=your_api_key_here
# =========================================== # ===========================================
# FEATURE TOGGLES # FEATURE TOGGLES
@ -80,7 +82,7 @@ SHOW_DX_PATHS=true
# =========================================== # ===========================================
# Your callsign for DX cluster login (default: CALLSIGN-56) # Your callsign for DX cluster login (default: CALLSIGN-56)
DX_CLUSTER_CALLSIGN= # DX_CLUSTER_CALLSIGN=N0CALL-56
# Spot retention time in minutes (5-30) # Spot retention time in minutes (5-30)
SPOT_RETENTION_MINUTES=30 SPOT_RETENTION_MINUTES=30

@ -9,6 +9,7 @@ All notable changes to OpenHamClock will be documented in this file.
- `.env` is auto-created from `.env.example` on first run - `.env` is auto-created from `.env.example` on first run
- Settings won't be overwritten by git updates - Settings won't be overwritten by git updates
- Supports: CALLSIGN, LOCATOR, PORT, HOST, UNITS, TIME_FORMAT, THEME, LAYOUT - Supports: CALLSIGN, LOCATOR, PORT, HOST, UNITS, TIME_FORMAT, THEME, LAYOUT
- **Auto-build on start** - `npm start` automatically builds the React frontend if needed
- **Update script** - Easy updates for local/Pi installations (`./scripts/update.sh`) - **Update script** - Easy updates for local/Pi installations (`./scripts/update.sh`)
- Backs up config, pulls latest, rebuilds, preserves settings - Backs up config, pulls latest, rebuilds, preserves settings
- **Network access configuration** - Set `HOST=0.0.0.0` to access from other devices - **Network access configuration** - Set `HOST=0.0.0.0` to access from other devices

@ -8,7 +8,7 @@ A modern, modular amateur radio dashboard built with React and Vite. This is the
# Install dependencies # Install dependencies
npm install npm install
# Start the server (auto-creates .env on first run) # Start the server (auto-builds frontend and creates .env on first run)
npm start npm start
# Edit .env with your callsign and grid locator # Edit .env with your callsign and grid locator
@ -18,14 +18,17 @@ npm start
# Open http://localhost:3000 # Open http://localhost:3000
``` ```
**That's it!** On first run, the server automatically creates a `.env` file from `.env.example`. Just edit it with your callsign and locator. **That's it!** On first run:
- Frontend is automatically built (React app compiled to `dist/`)
- `.env` file is created from `.env.example`
- Just edit `.env` with your callsign and locator
For development with hot reload: For development with hot reload:
```bash ```bash
# Terminal 1: Backend API server # Terminal 1: Backend API server
node server.js node server.js
# Terminal 2: Frontend dev server # Terminal 2: Frontend dev server with hot reload
npm run dev npm run dev
``` ```

@ -1,12 +1,13 @@
{ {
"name": "openhamclock", "name": "openhamclock",
"version": "3.7.0", "version": "3.10.0",
"description": "Amateur Radio Dashboard - A modern web-based HamClock alternative", "description": "Amateur Radio Dashboard - A modern web-based HamClock alternative",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"prestart": "node -e \"const fs=require('fs'); if(!fs.existsSync('dist/index.html')){console.log('Building frontend...'); require('child_process').execSync('npm run build',{stdio:'inherit'})}\"",
"start": "node server.js", "start": "node server.js",
"server": "node server.js", "server": "node server.js",
"test": "echo \"Tests passing\" && exit 0" "test": "echo \"Tests passing\" && exit 0"

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenHamClock - Build Required</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
text-align: center;
padding: 40px;
max-width: 600px;
}
h1 {
color: #fbbf24;
font-size: 2.5rem;
margin-bottom: 20px;
}
p {
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 15px;
color: #aaa;
}
code {
background: #2d2d44;
padding: 4px 10px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
color: #4ade80;
}
.command {
background: #2d2d44;
padding: 20px;
border-radius: 8px;
margin: 30px 0;
text-align: left;
}
.command code {
display: block;
background: none;
padding: 5px 0;
}
.note {
font-size: 0.9rem;
color: #888;
margin-top: 30px;
}
</style>
</head>
<body>
<div class="container">
<h1>📻 OpenHamClock</h1>
<p>The frontend needs to be built before running.</p>
<div class="command">
<code>npm install</code>
<code>npm run build</code>
<code>npm start</code>
</div>
<p>Or use the quick start:</p>
<div class="command">
<code>npm install && npm start</code>
</div>
<p class="note">
If you're seeing this page, the build step was skipped.<br>
Running <code>npm start</code> should auto-build if needed.
</p>
</div>
</body>
</html>

@ -156,7 +156,10 @@ if (configMissing) {
} }
// ITURHFProp service URL (optional - enables hybrid mode) // ITURHFProp service URL (optional - enables hybrid mode)
const ITURHFPROP_URL = process.env.ITURHFPROP_URL || null; // Must be a full URL like https://iturhfprop.example.com
const ITURHFPROP_URL = process.env.ITURHFPROP_URL && process.env.ITURHFPROP_URL.trim().startsWith('http')
? process.env.ITURHFPROP_URL.trim()
: null;
// Log configuration // Log configuration
console.log(`[Config] Station: ${CONFIG.callsign} @ ${CONFIG.gridSquare || 'No grid'}`); console.log(`[Config] Station: ${CONFIG.callsign} @ ${CONFIG.gridSquare || 'No grid'}`);
@ -192,17 +195,27 @@ function logErrorOnce(category, message) {
return false; return false;
} }
// Serve static files - use 'dist' in production (Vite build), 'public' in development // Serve static files
const staticDir = process.env.NODE_ENV === 'production' // dist/ contains the built React app (from npm run build)
? path.join(__dirname, 'dist') // public/ contains the fallback page if build hasn't run
: path.join(__dirname, 'public'); const distDir = path.join(__dirname, 'dist');
app.use(express.static(staticDir)); const publicDir = path.join(__dirname, 'public');
// Also serve public folder for any additional assets // Check if dist/ exists (has index.html from build)
if (process.env.NODE_ENV === 'production') { const distExists = fs.existsSync(path.join(distDir, 'index.html'));
app.use(express.static(path.join(__dirname, 'public')));
if (distExists) {
// Serve built React app from dist/
app.use(express.static(distDir));
console.log('[Server] Serving React app from dist/');
} else {
// No build found - serve placeholder from public/
console.log('[Server] ⚠️ No build found! Run: npm run build');
} }
// Always serve public folder (for fallback and assets)
app.use(express.static(publicDir));
// ============================================ // ============================================
// API PROXY ENDPOINTS // API PROXY ENDPOINTS
// ============================================ // ============================================
@ -1770,17 +1783,16 @@ let tleCache = { data: null, timestamp: 0 };
const TLE_CACHE_DURATION = 6 * 60 * 60 * 1000; // 6 hours const TLE_CACHE_DURATION = 6 * 60 * 60 * 1000; // 6 hours
app.get('/api/satellites/tle', async (req, res) => { app.get('/api/satellites/tle', async (req, res) => {
console.log('[Satellites] Fetching TLE data...');
try { try {
const now = Date.now(); const now = Date.now();
// Return cached data if fresh // Return cached data if fresh
if (tleCache.data && (now - tleCache.timestamp) < TLE_CACHE_DURATION) { if (tleCache.data && (now - tleCache.timestamp) < TLE_CACHE_DURATION) {
console.log('[Satellites] Returning cached TLE data');
return res.json(tleCache.data); return res.json(tleCache.data);
} }
console.log('[Satellites] Fetching fresh TLE data...');
// Fetch fresh TLE data from CelesTrak // Fetch fresh TLE data from CelesTrak
const tleData = {}; const tleData = {};
@ -1819,7 +1831,6 @@ app.get('/api/satellites/tle', async (req, res) => {
tle1: line1, tle1: line1,
tle2: line2 tle2: line2
}; };
console.log('[Satellites] Found TLE for:', key, noradId);
} }
} }
} }
@ -1829,10 +1840,18 @@ app.get('/api/satellites/tle', async (req, res) => {
// Also try to get ISS specifically (it's in the stations group) // Also try to get ISS specifically (it's in the stations group)
if (!tleData['ISS']) { if (!tleData['ISS']) {
try { try {
const issController = new AbortController();
const issTimeout = setTimeout(() => issController.abort(), 10000);
const issResponse = await fetch( const issResponse = await fetch(
'https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=tle', 'https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=tle',
{ headers: { 'User-Agent': 'OpenHamClock/3.3' } } {
headers: { 'User-Agent': 'OpenHamClock/3.3' },
signal: issController.signal
}
); );
clearTimeout(issTimeout);
if (issResponse.ok) { if (issResponse.ok) {
const issText = await issResponse.text(); const issText = await issResponse.text();
const issLines = issText.trim().split('\n'); const issLines = issText.trim().split('\n');
@ -1846,7 +1865,9 @@ app.get('/api/satellites/tle', async (req, res) => {
} }
} }
} catch (e) { } catch (e) {
console.log('[Satellites] Could not fetch ISS TLE:', e.message); if (e.name !== 'AbortError') {
logErrorOnce('Satellites', `ISS TLE fetch: ${e.message}`);
}
} }
} }
@ -1857,7 +1878,10 @@ app.get('/api/satellites/tle', async (req, res) => {
res.json(tleData); res.json(tleData);
} catch (error) { } catch (error) {
console.error('[Satellites] TLE fetch error:', error.message); // Don't spam logs for timeouts (AbortError) or network issues
if (error.name !== 'AbortError') {
logErrorOnce('Satellites', `TLE fetch error: ${error.message}`);
}
// Return cached data even if stale, or empty object // Return cached data even if stale, or empty object
res.json(tleCache.data || {}); res.json(tleCache.data || {});
} }
@ -3083,9 +3107,11 @@ app.get('/api/config', (req, res) => {
// ============================================ // ============================================
app.get('*', (req, res) => { app.get('*', (req, res) => {
const indexPath = process.env.NODE_ENV === 'production' // Try dist first (built React app), fallback to public (monolithic)
? path.join(__dirname, 'dist', 'index.html') const distIndex = path.join(__dirname, 'dist', 'index.html');
: path.join(__dirname, 'public', 'index.html'); const publicIndex = path.join(__dirname, 'public', 'index.html');
const indexPath = fs.existsSync(distIndex) ? distIndex : publicIndex;
res.sendFile(indexPath); res.sendFile(indexPath);
}); });

Loading…
Cancel
Save

Powered by TurnKey Linux.