diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5c67c23 --- /dev/null +++ b/.env.example @@ -0,0 +1,86 @@ +# OpenHamClock Configuration +# +# This file is automatically copied to .env on first run. +# Edit .env with your station info - it won't be overwritten by updates. +# +# After editing, restart the server: npm start + +# =========================================== +# REQUIRED - Your Station Information +# =========================================== + +# Your amateur radio callsign +CALLSIGN=N0CALL + +# Your Maidenhead grid locator (4 or 6 character) +LOCATOR=FN31 + +# Your station coordinates (optional - calculated from LOCATOR if not set) +LATITUDE= +LONGITUDE= + +# =========================================== +# SERVER SETTINGS +# =========================================== + +# Port to run the server on (default: 3000) +PORT=3000 + +# Host/IP to bind to +# localhost = only accessible from this computer +# 0.0.0.0 = accessible from other devices on your network +HOST=localhost + +# =========================================== +# DISPLAY PREFERENCES +# =========================================== + +# Units: 'imperial' or 'metric' +# Affects temperature (°F/°C) and distances (mi/km) +UNITS=imperial + +# Time format: '12' or '24' hour +TIME_FORMAT=12 + +# Theme: 'dark', 'light', 'legacy', or 'retro' +THEME=dark + +# Layout: 'modern' or 'classic' +LAYOUT=modern + +# =========================================== +# OPTIONAL - External Services +# =========================================== + +# ITURHFProp service URL (for advanced propagation predictions) +ITURHFPROP_URL= + +# DX Spider Proxy URL (for DX cluster spots) +DXSPIDER_PROXY_URL= + +# OpenWeatherMap API key (for local weather display) +# Get a free key at https://openweathermap.org/api +OPENWEATHER_API_KEY= + +# =========================================== +# FEATURE TOGGLES +# =========================================== + +# Show POTA spots on map (true/false) +SHOW_POTA=true + +# Show satellite tracks on map (true/false) +SHOW_SATELLITES=true + +# Show DX paths on map (true/false) +SHOW_DX_PATHS=true + +# =========================================== +# DX CLUSTER SETTINGS +# =========================================== + +# Your callsign for DX cluster login (default: CALLSIGN-56) +DX_CLUSTER_CALLSIGN= + +# Spot retention time in minutes (5-30) +SPOT_RETENTION_MINUTES=30 diff --git a/.gitignore b/.gitignore index 03e2829..336371b 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ temp/ # Config files that might contain secrets config.local.js config.local.json +config.json # Test files *.test.js.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 820defc..98ab860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,247 +2,94 @@ All notable changes to OpenHamClock will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Planned -- SOTA API integration -- WebSocket DX cluster connection -- Azimuthal equidistant projection option - -## [3.9.0] - 2026-02-01 +## [3.10.0] - 2025-02-02 ### Added -- **Hybrid Propagation System** - Best-of-both-worlds HF prediction - - Combines ITURHFProp (ITU-R P.533-14) base predictions with real-time ionosonde corrections - - Automatic fallback to built-in calculations when ITURHFProp unavailable - - Configurable via `ITURHFPROP_URL` environment variable -- **ITURHFProp Service** - Deployable microservice for ITU-R P.533-14 predictions - - REST API wrapper around ITURHFProp engine - - Docker/Railway deployable - - Endpoints: `/api/predict`, `/api/predict/hourly`, `/api/bands`, `/api/health` -- **Ionospheric Correction Factor** - Adjusts model predictions based on actual conditions - - Compares expected foF2 (from model) vs actual foF2 (from ionosonde) - - Applies geomagnetic (K-index) penalties - - Reports correction confidence (high/medium/low) +- **Environment-based configuration** - Station settings now stored in `.env` 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 +- **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 +- **Grid locator auto-conversion** - Automatically calculates lat/lon from LOCATOR +- **Setup wizard** - Settings panel auto-opens if CALLSIGN or LOCATOR is missing +- **Retro theme** - 90s Windows style with teal background and silver panels +- **Classic layout** - Original HamClock-style with black background, large colored numbers, rainbow frequency bar ### Changed -- Propagation API now reports hybrid mode status -- Response includes `model` field indicating prediction source -- Added `hybrid` object to propagation response with correction details - -### Technical -- New functions: `fetchITURHFPropPrediction()`, `applyHybridCorrection()`, `calculateIonoCorrection()` -- 5-minute cache for ITURHFProp predictions -- Graceful degradation when services unavailable - -## [3.8.0] - 2026-01-31 - -### Added -- **DX Cluster Paths on Map** - Visual lines connecting spotters to DX stations - - Band-specific colors: 160m (red), 80m (orange), 40m (yellow), 20m (green), 15m (cyan), 10m (purple), 6m (magenta) - - Toggle visibility with button in DX Cluster panel - - Click paths to see spot details -- **Hover Highlighting** - Hover over spots in DX list to highlight path on map - - Path turns white and thickens when hovered - - Circle markers pulse on hover -- **Grid Square Extraction** - Parse grid squares from DX cluster comments - - Supports "Grid: XX00xx" format in spot comments - - Shows grid in spot popups on map -- **Callsign Labels on Map** - Optional labels for DX stations and spotters - - Toggle with label button in DX Cluster panel -- **Moon Tracking** - Real-time sublunar point on map - - Shows current moon phase emoji - - Updates position and phase in real-time +- Configuration priority: localStorage > .env > defaults +- Server startup now shows station callsign and network access info +- Settings panel updated with .env setup instructions +- DX Spider connection uses dxspider.co.uk as primary (thanks Keith G6NHU) +- SSID -56 for OpenHamClock connections (HamClock uses -55) -### Changed -- Improved DX path rendering with antimeridian crossing support -- Better popup formatting with grid square display -- Enhanced spot filtering works on map paths too - -## [3.7.0] - 2026-01-31 - -### Added -- **DX Spider Proxy Service** - Dedicated server for DX cluster data - - Real-time Telnet connection to DX Spider nodes - - WebSocket distribution to multiple clients - - Grid square parsing from spot comments - - Fallback to HTTP APIs when Telnet unavailable -- **Spotter Location Mapping** - Show where spots originate from - - Circle markers for spotters with callsign popups - - Lines connecting spotter to DX station -- **Map Layer Controls** - Toggle various map overlays - - POTA activators toggle - - DX cluster paths toggle - - Satellite footprints toggle (placeholder) - -### Technical -- New `/api/dxcluster-paths` endpoint returns enriched spot data -- Grid-to-coordinate conversion for spotter locations -- Improved caching for DX cluster data - -## [3.6.0] - 2026-01-31 - -### Added -- **Real-Time Ionosonde Data Integration** - Enhanced propagation predictions using actual ionospheric measurements - - Fetches real-time foF2, MUF(3000), hmF2 data from KC2G/GIRO ionosonde network (~100 stations worldwide) - - Inverse distance weighted interpolation for path midpoint ionospheric parameters - - 10-minute data cache with automatic refresh - - New `/api/ionosonde` endpoint to access raw station data +### Fixed +- Header clock "shaking" when digits change - now uses monospace font +- Header layout wrapping on smaller screens - added `whiteSpace: nowrap` +- Reduced log spam with rate-limited error logging (1 per minute per type) +- DX Spider connection errors silenced for common issues (ECONNRESET, ETIMEDOUT) -### Changed -- **ITU-R P.533-based MUF Calculation** - More accurate Maximum Usable Frequency estimation - - Uses real foF2 and M(3000)F2 values when available - - Distance-scaled MUF calculation for varying path lengths - - Fallback to solar index estimation when ionosonde data unavailable -- **Improved LUF Calculation** - Better Lowest Usable Frequency (D-layer absorption) model - - Accounts for solar zenith angle, solar flux, and geomagnetic activity - - Day/night variation with proper diurnal profile -- **Enhanced Reliability Algorithm** - ITU-R P.533 inspired reliability calculations - - Optimum Working Frequency (OWF) centered predictions - - Multi-hop path loss consideration - - Polar path and auroral absorption penalties - - Low-band nighttime enhancement - -### UI Improvements -- Propagation panel shows MUF and LUF values in MHz -- Data source indicator (📡 ionosonde name vs ⚡ estimated) -- Green dot indicator when using real ionosonde data -- foF2 value displayed when available (replaces SSN in bar view) -- Distance now shown in km (not Kkm) - -### Technical -- New `fetchIonosondeData()` function with caching -- `interpolateFoF2()` for spatial interpolation of ionospheric parameters -- `calculateMUF()` and `calculateLUF()` helper functions -- `calculateEnhancedReliability()` with proper diurnal scaling - -## [3.3.0] - 2026-01-30 +## [3.9.0] - 2025-01-31 ### Added -- **Contest Calendar** - Shows upcoming and active ham radio contests - - Integrates with WA7BNM Contest Calendar API - - Fallback calculation for major recurring contests (CQ WW, ARRL, etc.) - - Weekly mini-contests (CWT, SST, NCCC Sprint) - - Active contest highlighting with blinking indicator -- **Classic Layout** - New layout option inspired by original HamClock - - Side panels for DE/DX info, DX cluster, contests - - Large centered map - - Compact data-dense design -- **Theme System** - Three visual themes - - 🌙 Dark (default) - Modern dark theme with amber/cyan accents - - ☀️ Light - Bright theme for daytime use - - 📟 Legacy - Classic green-on-black CRT style -- **Quick Stats Panel** - Overview of active contests, POTA activators, DX spots -- **4-column modern layout** - Improved data organization -- **Settings persistence** - Theme and layout saved to localStorage +- DX Filter modal with tabs for Zones, Bands, Modes, Watchlist, Exclude +- Spot retention time configurable (5-30 minutes) in Settings +- Satellite tracking with 40+ amateur radio satellites +- Satellite footprints and orbit path visualization +- Map legend showing all 10 HF bands plus DE/DX/Sun/Moon markers ### Changed -- Modern layout now uses 4-column grid for better information density -- Improved DX cluster API with multiple fallback sources -- Settings panel now includes theme and layout selection - -## [3.2.0] - 2026-01-30 - -### Added -- Theme support (dark, light, legacy) -- Layout selection in settings -- Real-time theme preview in settings - -## [3.1.0] - 2026-01-30 - -### Added -- User settings panel with callsign and location configuration -- Grid square entry with automatic lat/lon conversion -- Browser geolocation support ("Use My Current Location") -- Settings saved to localStorage +- Enlarged Header, DE, and DX panels with bigger fonts +- Improved callsign label positioning on map ### Fixed -- DX cluster now uses server proxy only (no CORS errors) -- Improved DX cluster API reliability with multiple sources +- DX Filter modal crash when opening +- K-Index display showing correct values +- Contest calendar attribution -## [3.0.0] - 2026-01-30 +## [3.8.0] - 2025-01-28 ### Added -- **Real map tiles** via Leaflet.js - no more approximated shapes! -- **8 map styles**: Dark, Satellite, Terrain, Streets, Topo, Ocean, NatGeo, Gray -- **Interactive map** - click anywhere to set DX location -- **Day/night terminator** using Leaflet.Terminator plugin -- **Great circle path** visualization between DE and DX -- **POTA activators** displayed on map with callsigns -- **Express server** with API proxy for CORS-free data fetching -- **Electron desktop app** support for Windows, macOS, Linux -- **Docker support** with multi-stage build -- **Railway deployment** configuration -- **Raspberry Pi setup script** with kiosk mode option -- **Cross-platform install scripts** (Linux, macOS, Windows) -- **GitHub Actions CI/CD** pipeline +- Multiple DX cluster source fallbacks +- ITURHFProp hybrid propagation predictions +- Ionosonde real-time corrections ### Changed -- Complete rewrite of map rendering using Leaflet.js -- Improved responsive layout for different screen sizes -- Better error handling for API failures -- Cleaner separation of frontend and backend - -### Fixed -- CORS issues with external APIs now handled by server proxy -- Map projection accuracy improved +- DX cluster cache extended to 90 seconds +- Improved error handling for external APIs -## [2.0.0] - 2026-01-29 +## [3.7.0] - 2025-01-25 ### Added -- Live API integrations for NOAA space weather -- POTA API integration for activator spots -- Band conditions from HamQSL (XML parsing) -- DX cluster spot display -- Realistic continent shapes (SVG paths) -- Great circle path calculations -- Interactive map (click to set DX) +- Modular React architecture with Vite +- 13 extracted components +- 12 custom data-fetching hooks +- 3 utility modules +- Railway deployment support +- Docker support ### Changed -- Improved space weather display with color coding -- Better visual hierarchy in panels +- Complete rewrite from monolithic HTML to modular React +- CSS variables for theming +- Separated concerns for easier contribution -## [1.0.0] - 2026-01-29 +## [3.0.0] - 2025-01-15 ### Added -- Initial release -- World map with day/night terminator -- UTC and local time display -- DE/DX location panels with grid squares -- Short path / Long path bearing calculations -- Distance calculations -- Sunrise/sunset calculations -- Space weather panel (mock data) -- Band conditions panel -- DX cluster panel (mock data) -- POTA activity panel (mock data) -- Responsive grid layout -- Dark theme with amber/green accents - -### Acknowledgments -- Created in memory of Elwood Downey, WB0OEW -- Inspired by the original HamClock +- Initial modular extraction from monolithic codebase +- React + Vite build system +- Express backend for API proxying +- Three themes: Dark, Light, Legacy --- -## Version History Summary - -| Version | Date | Highlights | -|---------|------|------------| -| 3.9.0 | 2026-02-01 | Hybrid propagation (ITURHFProp + ionosonde) | -| 3.8.0 | 2026-01-31 | DX paths on map, hover highlights, moon tracking | -| 3.7.0 | 2026-01-31 | DX Spider proxy, spotter locations, map toggles | -| 3.6.0 | 2026-01-31 | Real-time ionosonde data, ITU-R P.533 propagation | -| 3.3.0 | 2026-01-30 | Contest calendar, classic layout, themes | -| 3.2.0 | 2026-01-30 | Theme system (dark/light/legacy) | -| 3.1.0 | 2026-01-30 | User settings, DX cluster fixes | -| 3.0.0 | 2026-01-30 | Real maps, Electron, Docker, Railway | -| 2.0.0 | 2026-01-29 | Live APIs, improved map | -| 1.0.0 | 2026-01-29 | Initial release | - ---- +## Version History -*73 de OpenHamClock contributors* +- **3.10.x** - Environment configuration, themes, layouts +- **3.9.x** - DX filtering, satellites, map improvements +- **3.8.x** - Propagation predictions, reliability improvements +- **3.7.x** - Modular React architecture +- **3.0.x** - Initial modular version +- **2.x** - Monolithic HTML version (archived) +- **1.x** - Original HamClock fork diff --git a/README.md b/README.md index c94bc63..2013040 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,97 @@ A modern, modular amateur radio dashboard built with React and Vite. This is the # Install dependencies npm install -# Start development servers (need two terminals) +# Start the server (auto-creates .env on first run) +npm start + +# Edit .env with your callsign and grid locator +# Then restart: +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. + +For development with hot reload: +```bash # Terminal 1: Backend API server node server.js -# Terminal 2: Frontend dev server with hot reload +# Terminal 2: Frontend dev server npm run dev +``` -# Open http://localhost:3000 +## ⚙️ Configuration + +### First Run (Automatic Setup) + +1. Run `npm start` - the server **automatically creates** `.env` from `.env.example` +2. Edit `.env` with your station info: + ```bash + CALLSIGN=K0CJH + LOCATOR=EN10 + ``` +3. Restart the server - you're ready to go! + +If you skip editing `.env`, the Settings panel will pop up in your browser asking for your callsign. + +### Configuration Priority + +Settings are loaded in this order (first one wins): +1. **localStorage** - Changes made in the browser Settings panel +2. **.env file** - Your station configuration (won't be overwritten by updates) +3. **Defaults** - Built-in fallback values + +### .env Options + +```bash +# Required - Your Station +CALLSIGN=N0CALL +LOCATOR=FN31 + +# Server Settings +PORT=3000 +HOST=localhost # Use 0.0.0.0 for network access + +# Display Preferences +UNITS=imperial # or 'metric' +TIME_FORMAT=12 # or '24' +THEME=dark # dark, light, legacy, retro +LAYOUT=modern # modern or classic + +# Optional Features +SHOW_SATELLITES=true +SHOW_POTA=true +SHOW_DX_PATHS=true + +# Optional Services +ITURHFPROP_URL= # For advanced propagation +DXSPIDER_PROXY_URL= # Custom DX cluster proxy +OPENWEATHER_API_KEY= # For local weather ``` -For production: +### Network Access + +To access OpenHamClock from other devices on your network: + ```bash -npm run build -npm start # Serves from dist/ on port 3001 +# In .env: +HOST=0.0.0.0 +PORT=3000 ``` +Then open `http://:3000` from any device. + +### Configuration Files + +| File | Git Tracked | Purpose | +|------|-------------|---------| +| `.env.example` | ✅ Yes | Template with all options documented | +| `.env` | ❌ No | Your config (auto-created, never overwritten) | +| `config.example.json` | ✅ Yes | Legacy JSON config template | +| `config.json` | ❌ No | Legacy JSON config (optional) | + ## 📁 Project Structure ``` @@ -75,13 +150,20 @@ openhamclock-modular/ ## 🎨 Themes -Three themes available via Settings: +Four themes available via Settings or `.env`: - **Dark** (default) - Modern dark theme with amber accents - **Light** - Light theme for daytime use - **Legacy** - Classic HamClock green-on-black terminal style +- **Retro** - 90s Windows style with teal and silver Themes use CSS custom properties defined in `src/styles/main.css`. +## 📐 Layouts + +Two layouts available: +- **Modern** (default) - Responsive 3-column grid +- **Classic** - Original HamClock-style with black background, large colored numbers, rainbow frequency bar + ## 🔌 Components All components are fully extracted and ready to modify: @@ -142,6 +224,25 @@ The backend server provides: ## 🚀 Deployment +### Raspberry Pi + +One-line install for Raspberry Pi: +```bash +curl -sSL https://raw.githubusercontent.com/k0cjh/openhamclock/main/scripts/setup-pi.sh | bash +``` + +Or with kiosk mode (auto-starts fullscreen on boot): +```bash +curl -sSL https://raw.githubusercontent.com/k0cjh/openhamclock/main/scripts/setup-pi.sh | bash -s -- --kiosk +``` + +After installation: +```bash +cd ~/openhamclock +nano .env # Edit your callsign and locator +./restart.sh +``` + ### Railway ```bash # railway.toml and railway.json are included @@ -159,6 +260,34 @@ npm run build NODE_ENV=production node server.js ``` +## 🔄 Updating + +For local/Pi installations, use the update script: + +```bash +cd ~/openhamclock +./scripts/update.sh +``` + +The update script will: +1. ✅ Back up your `.env` configuration +2. ✅ Pull the latest code from GitHub +3. ✅ Install any new dependencies +4. ✅ Rebuild the frontend +5. ✅ Preserve your settings + +Then restart the server: +```bash +sudo systemctl restart openhamclock +# or +./restart.sh +``` + +**Note:** If you installed from a zip file (not git clone), you'll need to: +1. Back up your `.env` file +2. Download the new zip +3. Extract and restore your `.env` + ## 🤝 Contributing 1. Fork the repository diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..30d9f2c --- /dev/null +++ b/config.example.json @@ -0,0 +1,42 @@ +{ + "_comment": "OpenHamClock User Configuration", + "_instructions": "Copy this file to config.json and customize for your station. Your config.json will NOT be overwritten by git updates.", + + "callsign": "N0CALL", + "locator": "FN31", + + "location": { + "lat": 40.0150, + "lon": -105.2705 + }, + + "defaultDX": { + "lat": 35.6762, + "lon": 139.6503 + }, + + "units": "imperial", + "timeFormat": "12", + "theme": "dark", + "layout": "modern", + + "features": { + "showPOTA": true, + "showSatellites": true, + "showDXPaths": true, + "showContests": true, + "showDXpeditions": true + }, + + "dxCluster": { + "spotRetentionMinutes": 30, + "source": "auto" + }, + + "refreshIntervals": { + "spaceWeather": 300000, + "bandConditions": 300000, + "pota": 60000, + "dxCluster": 30000 + } +} diff --git a/package.json b/package.json index 902b484..ed16391 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "axios": "^1.6.2", "cors": "^2.8.5", + "dotenv": "^16.3.1", "express": "^4.18.2", "node-fetch": "^2.7.0", "satellite.js": "^5.0.0", diff --git a/scripts/setup-pi.sh b/scripts/setup-pi.sh index 5979570..776cf31 100644 --- a/scripts/setup-pi.sh +++ b/scripts/setup-pi.sh @@ -140,12 +140,18 @@ setup_repository() { git pull else echo "Cloning repository..." - git clone https://github.com/accius/openhamclock.git "$INSTALL_DIR" + git clone https://github.com/k0cjh/openhamclock.git "$INSTALL_DIR" cd "$INSTALL_DIR" fi # Install npm dependencies - npm install --production + npm install + + # Build frontend for production + npm run build + + # Make update script executable + chmod +x scripts/update.sh 2>/dev/null || true echo -e "${GREEN}✓ OpenHamClock installed to $INSTALL_DIR${NC}" } @@ -317,10 +323,11 @@ print_summary() { echo -e " ${BLUE}Web Interface:${NC} http://localhost:3000" echo "" echo -e " ${YELLOW}Helper Commands:${NC}" - echo " $INSTALL_DIR/start.sh - Start server manually" - echo " $INSTALL_DIR/stop.sh - Stop everything" - echo " $INSTALL_DIR/restart.sh - Restart server" - echo " $INSTALL_DIR/status.sh - Check status" + echo " $INSTALL_DIR/scripts/update.sh - Update to latest version" + echo " $INSTALL_DIR/start.sh - Start server manually" + echo " $INSTALL_DIR/stop.sh - Stop everything" + echo " $INSTALL_DIR/restart.sh - Restart server" + echo " $INSTALL_DIR/status.sh - Check status" echo "" echo -e " ${YELLOW}Service Commands:${NC}" echo " sudo systemctl start ${SERVICE_NAME}" diff --git a/scripts/update.sh b/scripts/update.sh new file mode 100644 index 0000000..bd7cf11 --- /dev/null +++ b/scripts/update.sh @@ -0,0 +1,138 @@ +#!/bin/bash +# OpenHamClock Update Script +# Updates to the latest version while preserving your configuration + +set -e + +echo "╔═══════════════════════════════════════════════════════╗" +echo "║ OpenHamClock Update Script ║" +echo "╚═══════════════════════════════════════════════════════╝" +echo "" + +# Check if we're in the right directory +if [ ! -f "server.js" ] || [ ! -f "package.json" ]; then + echo "❌ Error: Please run this script from the openhamclock directory" + echo " cd /path/to/openhamclock" + echo " ./scripts/update.sh" + exit 1 +fi + +# Check if git is available +if ! command -v git &> /dev/null; then + echo "❌ Error: git is not installed" + echo " sudo apt install git" + exit 1 +fi + +# Check if this is a git repository +if [ ! -d ".git" ]; then + echo "❌ Error: This doesn't appear to be a git repository" + echo " If you installed from a zip file, you'll need to:" + echo " 1. Back up your .env file" + echo " 2. Download the new version" + echo " 3. Extract and copy your .env back" + exit 1 +fi + +echo "📋 Current version:" +grep '"version"' package.json | head -1 + +echo "" +echo "🔍 Checking for updates..." + +# Fetch latest changes +git fetch origin + +# Check if there are updates +LOCAL=$(git rev-parse HEAD) +REMOTE=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master) + +if [ "$LOCAL" = "$REMOTE" ]; then + echo "✅ Already up to date!" + exit 0 +fi + +echo "📦 Updates available!" +echo "" + +# Show what's new +echo "📝 Changes since your version:" +git log --oneline HEAD..origin/main 2>/dev/null || git log --oneline HEAD..origin/master +echo "" + +# Confirm update +read -p "🔄 Do you want to update? (y/N) " -n 1 -r +echo "" +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "❌ Update cancelled" + exit 0 +fi + +echo "" +echo "🛡️ Backing up configuration..." + +# Backup .env if it exists +if [ -f ".env" ]; then + cp .env .env.backup + echo " ✓ .env → .env.backup" +fi + +# Backup any other local config +if [ -f "config.json" ]; then + cp config.json config.json.backup + echo " ✓ config.json → config.json.backup" +fi + +echo "" +echo "⬇️ Pulling latest changes..." +git pull origin main 2>/dev/null || git pull origin master + +echo "" +echo "📦 Installing dependencies..." +npm install + +echo "" +echo "🔨 Building frontend..." +npm run build + +echo "" +echo "🔄 Restoring configuration..." + +# Restore .env (should still be there since it's gitignored, but just in case) +if [ -f ".env.backup" ] && [ ! -f ".env" ]; then + cp .env.backup .env + echo " ✓ .env restored from backup" +fi + +# Restore config.json if needed +if [ -f "config.json.backup" ] && [ ! -f "config.json" ]; then + cp config.json.backup config.json + echo " ✓ config.json restored from backup" +fi + +echo "" +echo "📋 New version:" +grep '"version"' package.json | head -1 + +echo "" +echo "╔═══════════════════════════════════════════════════════╗" +echo "║ ✅ Update Complete! ║" +echo "╚═══════════════════════════════════════════════════════╝" +echo "" +echo "🔄 Restart the server to apply changes:" +echo "" + +# Check if running as systemd service +if systemctl is-active --quiet openhamclock 2>/dev/null; then + echo " sudo systemctl restart openhamclock" +else + echo " # If running in terminal, press Ctrl+C and run:" + echo " npm start" + echo "" + echo " # If running as a service:" + echo " sudo systemctl restart openhamclock" +fi + +echo "" +echo "📖 See CHANGELOG.md for what's new" +echo "" diff --git a/server.js b/server.js index ff43b9d..996af8b 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,5 @@ /** - * OpenHamClock Server v3.9.0 + * OpenHamClock Server v3.10.0 * * Express server that: * 1. Serves the static web application @@ -7,15 +7,13 @@ * 3. Provides hybrid HF propagation predictions (ITURHFProp + real-time ionosonde) * 4. Provides WebSocket support for future real-time features * - * Propagation Model: Hybrid ITU-R P.533-14 - * - ITURHFProp service provides base P.533-14 predictions - * - KC2G/GIRO ionosonde network provides real-time corrections - * - Combines both for best accuracy + * Configuration: + * - Copy .env.example to .env and customize + * - Environment variables override .env file * * Usage: * node server.js * PORT=8080 node server.js - * ITURHFPROP_URL=https://your-service.railway.app node server.js */ const express = require('express'); @@ -23,14 +21,147 @@ const cors = require('cors'); const path = require('path'); const fetch = require('node-fetch'); const net = require('net'); +const fs = require('fs'); + +// Auto-create .env from .env.example on first run +const envPath = path.join(__dirname, '.env'); +const envExamplePath = path.join(__dirname, '.env.example'); + +if (!fs.existsSync(envPath) && fs.existsSync(envExamplePath)) { + fs.copyFileSync(envExamplePath, envPath); + console.log('[Config] Created .env from .env.example'); + console.log('[Config] ⚠️ Please edit .env with your callsign and locator, then restart'); +} + +// Load .env file if it exists +if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, 'utf8'); + envContent.split('\n').forEach(line => { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + const [key, ...valueParts] = trimmed.split('='); + const value = valueParts.join('='); + if (key && value !== undefined && !process.env[key]) { + process.env[key] = value; + } + } + }); + console.log('[Config] Loaded configuration from .env file'); +} const app = express(); const PORT = process.env.PORT || 3000; +const HOST = process.env.HOST || '0.0.0.0'; + +// ============================================ +// CONFIGURATION FROM ENVIRONMENT +// ============================================ + +// Convert Maidenhead grid locator to lat/lon +function gridToLatLon(grid) { + if (!grid || grid.length < 4) return null; + + grid = grid.toUpperCase(); + const lon = (grid.charCodeAt(0) - 65) * 20 - 180; + const lat = (grid.charCodeAt(1) - 65) * 10 - 90; + const lon2 = parseInt(grid[2]) * 2; + const lat2 = parseInt(grid[3]); + + let longitude = lon + lon2 + 1; // Center of grid + let latitude = lat + lat2 + 0.5; + + // 6-character grid for more precision + if (grid.length >= 6) { + const lon3 = (grid.charCodeAt(4) - 65) * (2/24); + const lat3 = (grid.charCodeAt(5) - 65) * (1/24); + longitude = lon + lon2 + lon3 + (1/24); + latitude = lat + lat2 + lat3 + (0.5/24); + } + + return { latitude, longitude }; +} + +// Get locator from env (support both LOCATOR and GRID_SQUARE) +const locator = process.env.LOCATOR || process.env.GRID_SQUARE || ''; + +// Also load config.json if it exists (for user preferences) +let jsonConfig = {}; +const configJsonPath = path.join(__dirname, 'config.json'); +if (fs.existsSync(configJsonPath)) { + try { + jsonConfig = JSON.parse(fs.readFileSync(configJsonPath, 'utf8')); + console.log('[Config] Loaded user preferences from config.json'); + } catch (e) { + console.error('[Config] Error parsing config.json:', e.message); + } +} + +// Calculate lat/lon from locator if not explicitly set +let stationLat = parseFloat(process.env.LATITUDE); +let stationLon = parseFloat(process.env.LONGITUDE); + +if ((!stationLat || !stationLon) && locator) { + const coords = gridToLatLon(locator); + if (coords) { + stationLat = stationLat || coords.latitude; + stationLon = stationLon || coords.longitude; + } +} + +// Fallback to config.json location if no env +if (!stationLat && jsonConfig.location?.lat) stationLat = jsonConfig.location.lat; +if (!stationLon && jsonConfig.location?.lon) stationLon = jsonConfig.location.lon; + +const CONFIG = { + // Station info (env takes precedence over config.json) + callsign: process.env.CALLSIGN || jsonConfig.callsign || 'N0CALL', + gridSquare: locator || jsonConfig.locator || '', + latitude: stationLat || 40.7128, + longitude: stationLon || -74.0060, + + // Display preferences + units: process.env.UNITS || jsonConfig.units || 'imperial', + timeFormat: process.env.TIME_FORMAT || jsonConfig.timeFormat || '12', + theme: process.env.THEME || jsonConfig.theme || 'dark', + layout: process.env.LAYOUT || jsonConfig.layout || 'modern', + + // DX target + dxLatitude: parseFloat(process.env.DX_LATITUDE) || jsonConfig.defaultDX?.lat || 51.5074, + dxLongitude: parseFloat(process.env.DX_LONGITUDE) || jsonConfig.defaultDX?.lon || -0.1278, + + // Feature toggles + showSatellites: process.env.SHOW_SATELLITES !== 'false' && jsonConfig.features?.showSatellites !== false, + showPota: process.env.SHOW_POTA !== 'false' && jsonConfig.features?.showPOTA !== false, + showDxPaths: process.env.SHOW_DX_PATHS !== 'false' && jsonConfig.features?.showDXPaths !== false, + showContests: jsonConfig.features?.showContests !== false, + showDXpeditions: jsonConfig.features?.showDXpeditions !== false, + + // DX Cluster settings + spotRetentionMinutes: parseInt(process.env.SPOT_RETENTION_MINUTES) || jsonConfig.dxCluster?.spotRetentionMinutes || 30, + dxClusterSource: jsonConfig.dxCluster?.source || 'auto', + + // API keys (don't expose to frontend) + _openWeatherApiKey: process.env.OPENWEATHER_API_KEY || '', + _qrzUsername: process.env.QRZ_USERNAME || '', + _qrzPassword: process.env.QRZ_PASSWORD || '' +}; + +// Check if required config is missing +const configMissing = CONFIG.callsign === 'N0CALL' || !CONFIG.gridSquare; +if (configMissing) { + console.log('[Config] ⚠️ Station configuration incomplete!'); + console.log('[Config] Copy .env.example to .env OR config.example.json to config.json'); + console.log('[Config] Set your CALLSIGN and LOCATOR/grid square'); + console.log('[Config] Settings popup will appear in browser'); +} // ITURHFProp service URL (optional - enables hybrid mode) const ITURHFPROP_URL = process.env.ITURHFPROP_URL || null; // Log configuration +console.log(`[Config] Station: ${CONFIG.callsign} @ ${CONFIG.gridSquare || 'No grid'}`); +console.log(`[Config] Location: ${CONFIG.latitude.toFixed(4)}, ${CONFIG.longitude.toFixed(4)}`); +console.log(`[Config] Units: ${CONFIG.units}, Time: ${CONFIG.timeFormat}h`); if (ITURHFPROP_URL) { console.log(`[Propagation] Hybrid mode enabled - ITURHFProp service: ${ITURHFPROP_URL}`); } else { @@ -2889,17 +3020,55 @@ app.get('/api/health', (req, res) => { // CONFIGURATION ENDPOINT // ============================================ +// Serve station configuration to frontend +// This allows the frontend to get config from .env/config.json without exposing secrets app.get('/api/config', (req, res) => { + // Don't expose API keys/passwords - only public config res.json({ - version: '3.0.0', + version: '3.10.0', + + // Station info (from .env or config.json) + callsign: CONFIG.callsign, + locator: CONFIG.gridSquare, + latitude: CONFIG.latitude, + longitude: CONFIG.longitude, + + // Display preferences + units: CONFIG.units, + timeFormat: CONFIG.timeFormat, + theme: CONFIG.theme, + layout: CONFIG.layout, + + // DX target + dxLatitude: CONFIG.dxLatitude, + dxLongitude: CONFIG.dxLongitude, + + // Feature toggles + showSatellites: CONFIG.showSatellites, + showPota: CONFIG.showPota, + showDxPaths: CONFIG.showDxPaths, + showContests: CONFIG.showContests, + showDXpeditions: CONFIG.showDXpeditions, + + // DX Cluster settings + spotRetentionMinutes: CONFIG.spotRetentionMinutes, + dxClusterSource: CONFIG.dxClusterSource, + + // Whether config is incomplete (show setup wizard) + configIncomplete: CONFIG.callsign === 'N0CALL' || !CONFIG.gridSquare, + + // Feature availability features: { spaceWeather: true, pota: true, sota: true, dxCluster: true, - satellites: false, // Coming soon - contests: false // Coming soon + satellites: true, + contests: true, + dxpeditions: true }, + + // Refresh intervals (ms) refreshIntervals: { spaceWeather: 300000, pota: 60000, @@ -2944,10 +3113,20 @@ app.listen(PORT, '0.0.0.0', () => { console.log('║ ║'); console.log('╚═══════════════════════════════════════════════════════╝'); console.log(''); - console.log(` 🌐 Server running at http://localhost:${PORT}`); + const displayHost = HOST === '0.0.0.0' ? 'localhost' : HOST; + console.log(` 🌐 Server running at http://${displayHost}:${PORT}`); + if (HOST === '0.0.0.0') { + console.log(` 🔗 Network access: http://:${PORT}`); + } console.log(' 📡 API proxy enabled for NOAA, POTA, SOTA, DX Cluster'); console.log(' 🖥️ Open your browser to start using OpenHamClock'); console.log(''); + if (CONFIG.callsign !== 'N0CALL') { + console.log(` 📻 Station: ${CONFIG.callsign} @ ${CONFIG.gridSquare}`); + } else { + console.log(' ⚠️ Configure your station in .env file'); + } + console.log(''); console.log(' In memory of Elwood Downey, WB0OEW'); console.log(' 73 de OpenHamClock contributors'); console.log(''); diff --git a/src/App.jsx b/src/App.jsx index 4cd4468..4cdf42b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -39,17 +39,40 @@ import { loadConfig, saveConfig, applyTheme, + fetchServerConfig, calculateGridSquare, calculateSunTimes } from './utils'; const App = () => { - // Configuration state + // Configuration state - initially use defaults, then load from server const [config, setConfig] = useState(loadConfig); + const [configLoaded, setConfigLoaded] = useState(false); const [currentTime, setCurrentTime] = useState(new Date()); const [startTime] = useState(Date.now()); const [uptime, setUptime] = useState('0d 0h 0m'); + // Load server configuration on startup (only matters for first-time users) + useEffect(() => { + const initConfig = async () => { + // Fetch server config (provides defaults for new users without localStorage) + await fetchServerConfig(); + + // Load config - localStorage takes priority over server config + const loadedConfig = loadConfig(); + setConfig(loadedConfig); + setConfigLoaded(true); + + // Only show settings if user has no saved config AND no valid callsign + // This prevents the popup from appearing every refresh + const hasLocalStorage = localStorage.getItem('openhamclock_config'); + if (!hasLocalStorage && loadedConfig.callsign === 'N0CALL') { + setShowSettings(true); + } + }; + initConfig(); + }, []); + // DX Location with localStorage persistence const [dxLocation, setDxLocation] = useState(() => { try { @@ -129,15 +152,12 @@ const App = () => { applyTheme(config.theme || 'dark'); }, []); - useEffect(() => { - const saved = localStorage.getItem('openhamclock_config'); - if (!saved) setShowSettings(true); - }, []); - + // Config save handler - persists to localStorage const handleSaveConfig = (newConfig) => { setConfig(newConfig); saveConfig(newConfig); applyTheme(newConfig.theme || 'dark'); + console.log('[Config] Saved to localStorage:', newConfig.callsign); }; // Data hooks diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index de51447..3eb5f52 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -22,8 +22,10 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { setTheme(config.theme || 'dark'); setLayout(config.layout || 'modern'); setDxClusterSource(config.dxClusterSource || 'dxspider-proxy'); - // Calculate grid from coordinates - if (config.location?.lat && config.location?.lon) { + // Use locator from config, or calculate from coordinates + if (config.locator) { + setGridSquare(config.locator); + } else if (config.location?.lat && config.location?.lon) { setGridSquare(calculateGridSquare(config.location.lat, config.location.lon)); } } @@ -93,6 +95,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { onSave({ ...config, callsign: callsign.toUpperCase(), + locator: gridSquare.toUpperCase(), location: { lat: parseFloat(lat), lon: parseFloat(lon) }, theme, layout, @@ -140,7 +143,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {

{ ⚙ Station Settings

+ {/* First-time setup banner */} + {(config?.configIncomplete || config?.callsign === 'N0CALL' || !config?.locator) && ( +
+
+ 👋 Welcome to OpenHamClock! +
+
+ Please enter your callsign and grid square to get started. + Your settings will be saved in your browser. +
+
+ 💡 Tip: For permanent config, copy .env.example to .env and set CALLSIGN and LOCATOR +
+
+ )} + {/* Callsign */}