updated .env requirements

pull/18/head
accius 2 days ago
parent 23ebba09e5
commit 73e3fce32e

@ -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

1
.gitignore vendored

@ -62,6 +62,7 @@ temp/
# Config files that might contain secrets
config.local.js
config.local.json
config.json
# Test files
*.test.js.snap

@ -2,247 +2,92 @@
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
- **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

@ -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://<your-computer-ip>: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:

@ -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
}
}

@ -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",

@ -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://<your-ip>:${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('');

@ -39,17 +39,35 @@ 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
useEffect(() => {
const initConfig = async () => {
await fetchServerConfig();
const serverConfig = loadConfig();
setConfig(serverConfig);
setConfigLoaded(true);
// Auto-show settings if config is incomplete
if (serverConfig.configIncomplete || serverConfig.callsign === 'N0CALL') {
setShowSettings(true);
}
};
initConfig();
}, []);
// DX Location with localStorage persistence
const [dxLocation, setDxLocation] = useState(() => {
try {

@ -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 }) => {
<h2 style={{
color: 'var(--accent-cyan)',
marginTop: 0,
marginBottom: '24px',
marginBottom: '16px',
textAlign: 'center',
fontFamily: 'Orbitron, monospace',
fontSize: '20px'
@ -148,6 +151,29 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
Station Settings
</h2>
{/* First-time setup banner */}
{(config?.configIncomplete || config?.callsign === 'N0CALL' || !config?.locator) && (
<div style={{
background: 'rgba(255, 193, 7, 0.15)',
border: '1px solid var(--accent-amber)',
borderRadius: '8px',
padding: '12px 16px',
marginBottom: '20px',
fontSize: '13px'
}}>
<div style={{ color: 'var(--accent-amber)', fontWeight: '700', marginBottom: '6px' }}>
👋 Welcome to OpenHamClock!
</div>
<div style={{ color: 'var(--text-secondary)', lineHeight: 1.5 }}>
Please enter your callsign and grid square to get started.
Your settings will be saved in your browser.
</div>
<div style={{ color: 'var(--text-muted)', fontSize: '11px', marginTop: '8px' }}>
💡 Tip: For permanent config, copy <code style={{ background: 'var(--bg-tertiary)', padding: '2px 4px', borderRadius: '3px' }}>.env.example</code> to <code style={{ background: 'var(--bg-tertiary)', padding: '2px 4px', borderRadius: '3px' }}>.env</code> and set CALLSIGN and LOCATOR
</div>
</div>
)}
{/* Callsign */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>

@ -1,14 +1,25 @@
/**
* Configuration Utilities
* Handles app configuration, localStorage persistence, and theme management
*
* Configuration priority:
* 1. localStorage (user's browser settings)
* 2. Server config (from .env file)
* 3. Default values
*/
export const DEFAULT_CONFIG = {
callsign: 'N0CALL',
locator: '',
location: { lat: 40.0150, lon: -105.2705 }, // Boulder, CO (default)
defaultDX: { lat: 35.6762, lon: 139.6503 }, // Tokyo
units: 'imperial', // 'imperial' or 'metric'
theme: 'dark', // 'dark', 'light', 'legacy', or 'retro'
layout: 'modern', // 'modern' or 'legacy'
layout: 'modern', // 'modern' or 'classic'
use12Hour: true,
showSatellites: true,
showPota: true,
showDxPaths: true,
refreshIntervals: {
spaceWeather: 300000,
bandConditions: 300000,
@ -18,20 +29,70 @@ export const DEFAULT_CONFIG = {
}
};
// Cache for server config
let serverConfig = null;
/**
* Fetch configuration from server (.env file)
* This is called once on app startup
*/
export const fetchServerConfig = async () => {
try {
const response = await fetch('/api/config');
if (response.ok) {
serverConfig = await response.json();
console.log('[Config] Loaded from server:', serverConfig.callsign, '@', serverConfig.locator);
return serverConfig;
}
} catch (e) {
console.warn('[Config] Could not fetch server config, using defaults');
}
return null;
};
/**
* Load config from localStorage or use defaults
* Load config from localStorage, merged with server config
*/
export const loadConfig = () => {
let config = { ...DEFAULT_CONFIG };
// First, apply server config if available
if (serverConfig) {
config = {
...config,
callsign: serverConfig.callsign || config.callsign,
locator: serverConfig.locator || config.locator,
location: {
lat: serverConfig.latitude || config.location.lat,
lon: serverConfig.longitude || config.location.lon
},
defaultDX: {
lat: serverConfig.dxLatitude || config.defaultDX.lat,
lon: serverConfig.dxLongitude || config.defaultDX.lon
},
units: serverConfig.units || config.units,
theme: serverConfig.theme || config.theme,
layout: serverConfig.layout || config.layout,
use12Hour: serverConfig.timeFormat === '12',
showSatellites: serverConfig.showSatellites ?? config.showSatellites,
showPota: serverConfig.showPota ?? config.showPota,
showDxPaths: serverConfig.showDxPaths ?? config.showDxPaths,
configIncomplete: serverConfig.configIncomplete
};
}
// Then, override with localStorage (user's local changes)
try {
const saved = localStorage.getItem('openhamclock_config');
if (saved) {
const parsed = JSON.parse(saved);
return { ...DEFAULT_CONFIG, ...parsed };
config = { ...config, ...parsed };
}
} catch (e) {
console.error('Error loading config:', e);
console.error('Error loading config from localStorage:', e);
}
return DEFAULT_CONFIG;
return config;
};
/**
@ -45,6 +106,14 @@ export const saveConfig = (config) => {
}
};
/**
* Check if configuration is incomplete (show setup wizard)
*/
export const isConfigIncomplete = () => {
const config = loadConfig();
return config.callsign === 'N0CALL' || !config.locator;
};
/**
* Apply theme to document
*/
@ -100,8 +169,10 @@ export const MAP_STYLES = {
export default {
DEFAULT_CONFIG,
fetchServerConfig,
loadConfig,
saveConfig,
isConfigIncomplete,
applyTheme,
MAP_STYLES
};

@ -9,6 +9,8 @@ export {
loadConfig,
saveConfig,
applyTheme,
fetchServerConfig,
isConfigIncomplete,
MAP_STYLES
} from './config.js';

Loading…
Cancel
Save

Powered by TurnKey Linux.