diff --git a/.env.example b/.env.example
index 5c67c23..0d05b11 100644
--- a/.env.example
+++ b/.env.example
@@ -16,8 +16,8 @@ CALLSIGN=N0CALL
LOCATOR=FN31
# Your station coordinates (optional - calculated from LOCATOR if not set)
-LATITUDE=
-LONGITUDE=
+# LATITUDE=40.7128
+# LONGITUDE=-74.0060
# ===========================================
# SERVER SETTINGS
@@ -53,14 +53,16 @@ LAYOUT=modern
# ===========================================
# 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)
-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)
# Get a free key at https://openweathermap.org/api
-OPENWEATHER_API_KEY=
+# OPENWEATHER_API_KEY=your_api_key_here
# ===========================================
# FEATURE TOGGLES
@@ -80,7 +82,7 @@ SHOW_DX_PATHS=true
# ===========================================
# 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_MINUTES=30
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98ab860..e0cbc66 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
- Settings won't be overwritten by git updates
- 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`)
- Backs up config, pulls latest, rebuilds, preserves settings
- **Network access configuration** - Set `HOST=0.0.0.0` to access from other devices
diff --git a/README.md b/README.md
index 2013040..e81ed0a 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ A modern, modular amateur radio dashboard built with React and Vite. This is the
# Install dependencies
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
# Edit .env with your callsign and grid locator
@@ -18,14 +18,17 @@ npm start
# 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:
```bash
# Terminal 1: Backend API server
node server.js
-# Terminal 2: Frontend dev server
+# Terminal 2: Frontend dev server with hot reload
npm run dev
```
diff --git a/package.json b/package.json
index ed16391..d91f39a 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,13 @@
{
"name": "openhamclock",
- "version": "3.7.0",
+ "version": "3.10.0",
"description": "Amateur Radio Dashboard - A modern web-based HamClock alternative",
"main": "server.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"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",
"server": "node server.js",
"test": "echo \"Tests passing\" && exit 0"
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..8d6a48c
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+ OpenHamClock - Build Required
+
+
+
+
+
📻 OpenHamClock
+
The frontend needs to be built before running.
+
+
+ npm install
+ npm run build
+ npm start
+
+
+
Or use the quick start:
+
+ npm install && npm start
+
+
+
+ If you're seeing this page, the build step was skipped.
+ Running npm start should auto-build if needed.
+
+
+
+
diff --git a/server.js b/server.js
index 996af8b..a531118 100644
--- a/server.js
+++ b/server.js
@@ -156,7 +156,10 @@ if (configMissing) {
}
// 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
console.log(`[Config] Station: ${CONFIG.callsign} @ ${CONFIG.gridSquare || 'No grid'}`);
@@ -192,17 +195,27 @@ function logErrorOnce(category, message) {
return false;
}
-// Serve static files - use 'dist' in production (Vite build), 'public' in development
-const staticDir = process.env.NODE_ENV === 'production'
- ? path.join(__dirname, 'dist')
- : path.join(__dirname, 'public');
-app.use(express.static(staticDir));
+// Serve static files
+// dist/ contains the built React app (from npm run build)
+// public/ contains the fallback page if build hasn't run
+const distDir = path.join(__dirname, 'dist');
+const publicDir = path.join(__dirname, 'public');
-// Also serve public folder for any additional assets
-if (process.env.NODE_ENV === 'production') {
- app.use(express.static(path.join(__dirname, 'public')));
+// Check if dist/ exists (has index.html from build)
+const distExists = fs.existsSync(path.join(distDir, 'index.html'));
+
+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
// ============================================
@@ -1770,17 +1783,16 @@ let tleCache = { data: null, timestamp: 0 };
const TLE_CACHE_DURATION = 6 * 60 * 60 * 1000; // 6 hours
app.get('/api/satellites/tle', async (req, res) => {
- console.log('[Satellites] Fetching TLE data...');
-
try {
const now = Date.now();
// Return cached data if fresh
if (tleCache.data && (now - tleCache.timestamp) < TLE_CACHE_DURATION) {
- console.log('[Satellites] Returning cached TLE data');
return res.json(tleCache.data);
}
+ console.log('[Satellites] Fetching fresh TLE data...');
+
// Fetch fresh TLE data from CelesTrak
const tleData = {};
@@ -1819,7 +1831,6 @@ app.get('/api/satellites/tle', async (req, res) => {
tle1: line1,
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)
if (!tleData['ISS']) {
try {
+ const issController = new AbortController();
+ const issTimeout = setTimeout(() => issController.abort(), 10000);
+
const issResponse = await fetch(
'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) {
const issText = await issResponse.text();
const issLines = issText.trim().split('\n');
@@ -1846,7 +1865,9 @@ app.get('/api/satellites/tle', async (req, res) => {
}
}
} 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);
} 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
res.json(tleCache.data || {});
}
@@ -3083,9 +3107,11 @@ app.get('/api/config', (req, res) => {
// ============================================
app.get('*', (req, res) => {
- const indexPath = process.env.NODE_ENV === 'production'
- ? path.join(__dirname, 'dist', 'index.html')
- : path.join(__dirname, 'public', 'index.html');
+ // Try dist first (built React app), fallback to public (monolithic)
+ const distIndex = path.join(__dirname, 'dist', 'index.html');
+ const publicIndex = path.join(__dirname, 'public', 'index.html');
+
+ const indexPath = fs.existsSync(distIndex) ? distIndex : publicIndex;
res.sendFile(indexPath);
});