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