All notable changes to OpenHamClock will be documented in this file.
📅 Schedule Change: Starting with v15.5.10, OpenHamClock moves to a weekly release cycle. Updates will ship on Tuesday nights (EST) — one release per week for better testing and stability.
- CORS lockdown: Replaced wildcard
origin: truewith explicit origin allowlist (localhost, openhamclock.com/app). Prevents malicious websites from accessing the API via the user's browser. Custom origins configurable viaCORS_ORIGINSenv var. - SSRF elimination: Custom DX cluster hosts are now DNS-resolved to IPv4, validated against private/reserved ranges, and the connection uses the validated IP (not hostname) to prevent DNS rebinding. IPv6 fallback removed to eliminate representation bypass attacks.
- Rotator & QRZ auth:
/api/rotator/turn,/api/rotator/stop,/api/qrz/configure,/api/qrz/removenow requireAPI_WRITE_KEYauthentication. - Trust proxy auto-detect:
trust proxyenabled only on Railway (auto-detected), disabled on Pi/local installs to prevent rate-limit bypass via spoofedX-Forwarded-Forheaders. Override withTRUST_PROXYenv var. - SSE connection limiter: Per-IP cap on concurrent SSE streams (default 10, configurable via
MAX_SSE_PER_IP) to prevent resource exhaustion. - Telnet command injection: Control characters stripped from DX cluster login callsigns.
- DOM XSS fixes:
sanitizeColor()for N3FJP logged QSO line colors;esc()helper for APRS Newsfeed userscript. - ReDoS fix: Replaced
/\d+$/regex withsubstring()for IP anonymization. - URL encoding:
encodeURIComponent()applied to callsign parameters in localhost fetch calls. - RBN callsign validation: Input sanitized and length-checked on
/api/rbn/location/:callsign. - Health endpoint: Session details (partial IPs, user agents) gated behind
API_WRITE_KEYauth. - Dockerfile: Application now runs as non-root user (
nodejs, UID 1001). - Startup warning: Server prints visible warning when
API_WRITE_KEYis not set. - Rig-bridge CORS: Restricted to explicit origin allowlist (was wildcard
*). - Rig-bridge localhost binding: HTTP server binds to
127.0.0.1by default (was0.0.0.0). - Rig-bridge serial port validation: Paths validated against OS-specific patterns (COM*, /dev/tty*, /dev/cu.*).
- Rig-bridge relay SSRF: Relay URL validated to reject private/reserved addresses.
- LMSAL solar image fallback: Three-source failover for solar imagery: SDO direct → LMSAL Sun Today (Lockheed Martin) → Helioviewer API. Independent of NASA Goddard infrastructure.
- Lightning unit preferences: Proximity panel distances respect km/miles setting from allUnits.
- DXCC entity selector: Browse/search DXCC entities to set DX target in Modern and Dockable layouts.
- DX News text scale: Adjustable font size (0.7x–2.0x) with A-/A+ buttons. Persists in localStorage.
- Layout lock border panel: Lock/unlock toggle in dedicated FlexLayout border tab (Dockable layout).
- Rig-bridge multicast: WSJT-X relay supports UDP multicast for multi-app packet sharing.
- WSJT-X relay multicast UI: Configurable multicast address in Settings → Station. Relay download scripts include the multicast address automatically. macOS
mktempcompatibility fix. Interim solution — relay functionality will be consolidated into rig-bridge in a future release. - Rig-bridge simulated radio: Mock plugin for testing without hardware (
radio.type = "mock"). - DX cluster TCP keepalive: Persistent telnet sessions use OS-level keepalive and auto-reconnect after 5 min silence.
- DX cluster SSID: Callsign SSID (-56) appended automatically when not provided.
- Rotator enabled by default:
.env.examplehadROTATOR_PROVIDER=pstrotator_udpuncommented, causing fresh installs to send UDP to a hardcoded IP. All rotator lines now commented out. - Pi setup (armhf): NodeSource dropped 32-bit ARM support for Node 20+. Setup script now downloads armv7l binaries directly from nodejs.org with retry support.
- Pi setup (electron):
npm install --ignore-scriptsprevents electron-winstaller postinstall failures on ARM.ELECTRON_SKIP_BINARY_DOWNLOAD=1skips useless Electron download.npm prune --omit=devfrees ~500MB after build.
- Log flooding — 115K dropped messages in 30 minutes: Six hot-path loggers (RBN spot responses, callsign mismatch warnings, WSPR heatmap, PSK-MQTT SSE connect/disconnect) were writing directly to
console.logon every request instead of going through the log level system. All moved behindlogDebug/logInfo/logErrorOnce. Added global token-bucket rate limiter (burst 20, refill 10/sec) as a safety net — excess logs silently dropped with 60-second summary. - Moon Image retry storm: When NASA Dial-A-Moon API was down, every client request triggered a fresh fetch attempt. Added 5-minute negative cache — stale Moon images served during outages instead of returning errors.
- RBN callsign lookup storm: When QRZ/HamQTH was down, every uncached skimmer callsign triggered a failed lookup on every spot cycle. Failed lookups now cached for 10 minutes with automatic expiry.
- Header vertical centering: Text in header bar (callsign, clocks, solar stats, buttons) was misaligned after layout changes. Fixed with
alignItems: 'center'on stats and buttons rows,lineHeight: 1on large text spans,boxSizing: border-box, andautogrid row height. - TLE data failures: CelesTrak rate-limited/banned the cloud server IP from excessive TLE polling. See "TLE Multi-Source Failover" below.
- TLE multi-source failover: Satellite TLE data now automatically fails over across three sources: CelesTrak → CelesTrak legacy (.com) → AMSAT. Rate limit responses (429/403) trigger immediate failover. Cache extended 6h → 12h. Stale TLEs served up to 48 hours. 30-minute negative cache prevents hammering.
TLE_SOURCESenv var for self-hosters to reorder sources. - Ultrawide monitor layout: Sidebars scale proportionally with viewport using
clamp()(left: 260–480px, right: 280–500px). On 2560px displays, sidebars grow to ~960px combined instead of being capped at 660px. Panel height caps removed — DXpeditions, POTA, Contests flex to fill space. - Mobile single-module scroll: Mobile layout (<768px) rebuilt with full-width cards, 60vh map, scroll-snap momentum, and proper vertical stacking order.
- Russian translation (Русский 🇷🇺) — 379 keys, 100% coverage
- Georgian translation (ქართული 🇬🇪) — 379 keys, 100% coverage
- 13 languages total: en, de, es, fr, it, ja, ko, ms, nl, pt, sl, ru, ka — all at 100%
- Global log rate limiter: Token bucket wraps
console.log/warn/errorto prevent Railway/cloud log pipeline floods regardless of source. Burst of 20, refill 10/sec, 60-second drop summary. - WhatsNew notice banner: Release announcements can now include a highlighted notice bar (used for the Tuesday schedule announcement).
- APRS-IS live tracking: Full APRS integration via server-side APRS-IS connection (rotate.aprs2.net). Stations parsed in real-time with position, course, speed, altitude, and symbol. Watchlist groups for EmComm nets, ARES/RACES events, Field Day tracking.
- Wildfire map layer: Active wildfires worldwide via NASA EONET satellite detection. Markers with severity indicators under new Natural Hazards category.
- Floods & Storms map layer: Active floods and severe storms worldwide via NASA EONET. Grouped under Natural Hazards in Settings.
- PSKReporter TX/RX split view: Separate "Being Heard" and "Hearing" tabs with per-direction counts, replacing combined view.
- Map layers categorized & sorted: Settings groups layers by category (📡 Propagation, 📻 Amateur Radio, 🌤️ Weather, ☀️ Space Weather,
⚠️ Natural Hazards, 🪨 Geology, 🗺️ Overlays) with alphabetical sorting within each. - 100% translation coverage — all 11 languages: Every string fully translated. Previously 45–61% coverage with 292 missing keys total.
- Duplicate WSJT-X/PSK spots (#396): Content-based dedup IDs replace timestamp-based. QSO logging checks call+freq+mode within 60s. MQTT ingestion deduplicates before buffering.
- Windows update mechanism: Git operations use proper path resolution and restart handles Windows process semantics.
- DX Cluster time display: Spot timestamps now show relative time ("5m ago") with original UTC in parentheses.
- Memory leaks — three unbounded caches: Propagation heatmap (200-entry cap, 10-min purge), custom DX sessions (15-min reap), DX path cache (100-key cap, 5-min cleanup).
- Merge conflict cleanup: Duplicate zoom buttons, triplicated switch/case blocks, duplicate variable declarations, broken cache check.
- Live NASA Moon imagery: Dial-A-Moon 730×730 JPG with 1-hour server-side cache replaces static SVG.
- Map legend & band colors restored: Clickable band color legend, rotator bearing line, satellite tracks, My Spots markers.
- Settings export filenames include time: e.g.
hamclock-current-2026-02-19-143022.json— multiple exports no longer overwrite.
- Draggable panel disappear bug: Stale mousemove/mouseup listeners from layout switches teleported panels off-screen. Fixed with AbortController cleanup.
- Portable callsign location: PJ2/W9WI, DL/W1ABC now resolve to correct DXCC entity via new
extractOperatingPrefix(). - Rig control CW mode: Band plan JSON now labels CW segments correctly. Rewritten
mapModeToRig()for proper CW/SSB/DATA switching. - Rig Listener FT-DX10 & Windows serial: DTR assertion fix for CP210x adapters, npm path resolution on Windows.
- Emoji icons on Linux: Proper emoji font-family CSS stack, auto-installed
fonts-noto-color-emojiin Pi setup.
- Satellite info minimize button: Collapse floating window to slim header during map viewing.
- Leaflet load race condition: Map polls up to 5 seconds for vendor script with actionable error message if it fails.
- Prettier code formatting pipeline with
.prettierrc, pre-commit hooks (Husky + lint-staged), and CI enforcement
- Satellite tracker overhaul — Floating data window, blinking visibility indicators, pinned satellite tracking, GOES-18/19 weather satellites re-enabled
- SOTA summit details — Spots now include full summit info (name, altitude, coordinates, points) from the official SOTA summits database, refreshed daily
- POTA/WWFF click-to-tune — Park spots now properly trigger rig control when clicked
- Community tab — New tab in Settings with GitHub, Facebook, and Reddit links plus a contributor wall
- SEO & branding — Favicon, Open Graph social cards, JSON-LD structured data, canonical URL, robots.txt, sitemap.xml
- Contributors list — 23 contributors recognized in the Community tab
- WSJT-X rig tuning — Click-to-tune now sends the dial frequency instead of the audio offset for FT8/FT4 decodes
- Frequency display — POTA, SOTA, and WWFF panels now consistently show frequencies in MHz
- SOTA QRT filtering — Operators who have signed off (QRT) are filtered out of the spots list
- Favicon not showing — SEO and favicon tags were only in the monolithic fallback, not the Vite-built index.html that production serves
- Nested duplicate
openhamclock-main/directory (3.7MB waste) - Backup/debug files:
.jsxbak,.backup,.bak,tle_backup.txt, test scripts, debug patches - Stale dev notes:
TODAYS_PLUGIN_UPDATES.md,PLUGIN_DOCUMENTATION_SUMMARY.md,RIG_CONTROL_COMPARISON.md - Deprecated
rig-bridge/andrig-control/directories (replaced byrig-listener/) - Duplicate
vite.config.js(keeping onlyvite.config.mjs) - Duplicate
build-rig-listener.ymlworkflow
- New
docs/ARCHITECTURE.md— full codebase map for contributors - Rewritten
CONTRIBUTING.md— proper dev setup, code patterns, testing checklist .editorconfigfor consistent formatting across editors.gitignoreupdated to prevent future cruft accumulation- Updated
CHANGELOG.mdwith all missing versions (15.4.1 through 15.5.3)
- Railway OOM crashes — Memory leak investigation at 1800-2300 concurrent users:
callsignLocationCache(RBN skimmers) had no cap — added 2000-entry limit with oldest-first evictiongeoIPCachestored duplicate data in both a Map and a plain object — eliminated object, reconstruct only at save time- Stats save interval reduced from 60s to 5 minutes to reduce GC pressure
- Memory logging — Added periodic heap usage reporting for leak detection
- QRZ auth cooldown — Suppressed BadRequestError spam during rate limiting
- Heap limit increased to 2048MB for high-traffic deployments
- cty.dat DXCC entity database — Callsign identification now uses the full AD1C cty.dat database (~400 entities, thousands of prefixes). Replaces the old 120-entry prefix table.
- Smarter DX cluster filtering — Spotter/spot continent/zone filtering uses cty.dat for accurate identification
- MUF layer regression — The ionosonde-based MUF overlay was missing from Map Layers; restored
- VOACAP power levels — Changing TX power now produces dramatically different propagation maps as expected
- Direct rig control — Click any DX spot, POTA activation, or WSJT-X decode to tune your radio. Supports Yaesu, Kenwood, Elecraft, and Icom via USB serial.
- One-click rig listener download — Download the Rig Listener for Windows, Mac, or Linux from Settings. Double-click to run — auto-installs everything.
- Interactive setup wizard — Detects USB serial ports, asks radio brand/model, saves config, connects in 30 seconds.
- Live frequency & mode display — Real-time frequency/mode shown on the dashboard, polling every 500ms.
- Night darkness slider — Adjust nighttime shading intensity on the map.
- Hosted user cleanup — Rotator panel and local-only features hidden for hosted users.
- QRZ.com callsign lookups — 3-tier waterfall: QRZ → HamQTH → prefix estimation
- Antenna rotator panel — Real-time azimuth display and Shift+click map control
- Mouse wheel zoom sensitivity — Adjustable scroll-to-zoom speed
- Map lock — Prevent accidental panning/zooming
- Clickable QRZ callsigns — Callsigns link to QRZ.com profiles across all panels
- Contest calendar links — Contest names link to WA7BNM calendar
- World copy replication — All markers replicate across three world copies
- RBN firehose fix — No more lost spots from telnet buffer overflow
- VOACAP power reactivity — Heatmap updates immediately on power/mode change
- PSK Reporter direction fix — Map popups show correct remote station callsign
- Critical memory leak (OOM at 4GB after ~24h) — Multiple unbounded data structures in the PSK-MQTT proxy caused heap exhaustion:
recentSpotshad no cap on insert — spots were.push()ed with no limit, only trimmed to 500 every 5 minutes. With 1000+ subscribed callsigns, hundreds of thousands of spots accumulated between cleanups. Now capped at 200 per callsign at insert time.recentSpotsandspotBufferentries were never cleaned up when callsigns unsubscribed — every callsign that disconnected left behind up to 500 spots for up to 1 hour. Over 24 hours with thousands of unique visitors, this accumulated hundreds of MB of orphaned spot data. Now deleted immediately on unsubscribe.spotBufferentries for unsubscribed callsigns were never cleaned in the 5-minute cleanup cycle (flush only iteratedsubscribers, notspotBuffer). Now cleaned.mySpotsCache(HamQTH spot lookups) grew forever with no eviction. Now cleaned every 2 minutes.- Removed dead
pskReporterSpotscache (tx/rx Maps with cleanup timer) that was never written to
- Added memory monitoring — Logs RSS, heap usage, and data structure sizes every 15 minutes for leak detection
- Set explicit Node.js heap limit (1GB) in Dockerfile to fail fast on leaks instead of slow-dying at 4GB
- Double SSE connection eliminated —
PSKReporterPanelwas callingusePSKReporter()internally whileApp.jsxalready had one open for the same callsign. Every user opened 2 SSE connections. Now the panel receives data as a prop from the single app-level hook, halving SSE traffic and server memory. - MQTT connack timeout crash —
removeAllListeners()during client teardown stripped the error handler; when the old client later emittedconnack timeout, Node.js crashed on the unhandled error event. Now re-attaches a no-op error handler after stripping listeners. Added globaluncaughtExceptionhandler as safety net. - SSE flush interval increased from 10s to 15s to reduce network egress
- Solar image disappearing after ~20 minutes —
onErrorhandler permanently hid the<img>element withdisplay: noneon any load failure, with no retry or recovery. A single transient network blip would make the image vanish until page refresh. Also, the cache-buster timestamp was computed once at render time and never updated, so the image URL went stale. Now uses React state with a 15-minute refresh interval, shows a "Retrying..." placeholder on error, and auto-retries after 30 seconds. - DX Lock / Settings buttons hidden or overlapping in Classic and Tablet layouts — Buttons were positioned at
top: 10pxwith hardcodedleftvalues that collided with WorldMap's built-in SAT and CALLS toggle buttons. Classic layout also lackedzIndex, causing Leaflet to render over them. Moved both buttons to bottom-left of map in a flex group (Classic) or standalone (Tablet), avoiding all conflicts with WorldMap's top-row controls. - Pi kiosk update fix —
update.shwithgit stash --include-untrackedwas swallowing Pi helper scripts (kiosk.sh,start.sh,stop.sh, etc.) that live in the repo root but aren't tracked by git. The stash was never popped, so the scripts vanished after update, breaking kiosk auto-start on reboot. Now preserves and restores these scripts across the update. Also added them to.gitignoreso git never treats them as untracked files.
- ID Timer panel (Dockable layout) — 10-minute countdown timer that reminds operators to identify their station. Displays a large countdown with progress bar; in the final minute the display turns red and pulses. At expiration, plays three short beeps via Web Audio API and shows a full-screen overlay with your callsign in large blinking text. Clicking anywhere dismisses the alert and resets the timer. Start/Stop button lets operators pause the timer when not on the air. Available from the
+panel picker as 📢 ID Timer
- PSK-MQTT reconnect fork bomb — When the MQTT broker connection dropped,
pskMqttConnect()called.end(true)on the old client, which fired itscloseevent, which calledscheduleMqttReconnect(), which calledpskMqttConnect()again — each cycle doubling the number of pending reconnect chains. Over hours of downtime this created hundreds of parallel reconnect loops, flooding logs withDisconnected from mqtt.pskreporter.infoand exhausting resources. Three fixes: (1) strip all event listeners from old client before.end(true)so itscloseevent can't schedule a reconnect; (2) stale-client guard onclose/error/offlinehandlers — only react if the firing client is still the current one; (3) single reconnect timer —clearTimeoutbefore scheduling a new reconnect, preventing multiple pending timers
- SOTA (Summits on the Air) panel — New SOTA activator spots panel alongside POTA. In Classic and Modern layouts, the POTA slot now has POTA/SOTA tabs (like the Solar panel cycling) with independent Map ON/OFF toggles for each. In Dockable layout, SOTA is a fully separate panel in the panel picker (⛰️) so you can dock it independently, stack it with POTA, or place it anywhere
- SOTA map markers — Orange diamond markers for SOTA activators on the world map (distinct from POTA's green triangles). Callsign labels shown when DX labels are enabled. Popup shows summit reference, name, and points. Separate
showSOTAmap layer toggle. Legend shows◆ SOTAwhen active - SOTA data hook —
useSOTASpotsfetches from the existing/api/sota/spotsserver endpoint (SOTA API v2). Maps summit details including lat/lon, altitude, and activation points. 2-minute refresh cycle matching POTA
- DX location lock not working — Clicking callsigns in DX Cluster or PSK Reporter panels moved the DX point even when locked. Lock check was only in the WorldMap click handler;
handleDXChangeinuseDXLocationnow gates all updates throughdxLockedRef - Map overlays disappear at dateline (#327) — Replaced antimeridian path splitting with world-copy duplication (same approach as the GrayLine plugin). Polylines and circle markers are now rendered at -360°, 0°, +360° longitude offsets so they appear on every visible map copy. Affects DX Cluster paths, PSK Reporter paths, WSJT-X paths, satellite tracks/footprints, great circle lines, and contest QSO lines. Users in Australia, New Zealand, and Pacific islands no longer lose spots when panning
- Server-side settings sync (opt-in) — All UI preferences (layout, panel visibility, map layers, filters, theme, temp unit, solar panel mode, etc.) can now be persisted on the server in
data/settings.json. Enable withSETTINGS_SYNC=truein.env— designed for single-operator self-hosted/Pi deployments where you want every device (phone, tablet, desktop) to share the same configuration without setting up each browser individually. Disabled by default for multi-user hosted deployments like openhamclock.com where each user's settings live in their own browser localStorage. When enabled: server → browser on page load (server wins), browser → server on any change (2s debounce). First device to connect seeds the server; subsequent devices inherit settings
- Stale SFI and SSN values — SFI was reading from
f107_cm_flux.jsonwhich stopped updating at 2025-12-31, showing a month-old value of 170. SSN was reading fromobserved-solar-cycle-indices.jsonwhich only has monthly averages. Now uses three-tier fallback: (1) SWPCsummary/10cm-flux.jsonfor current SFI (updates every few hours), (2) N0NBH/hamqsl.com feed for both SFI and daily SSN (same source as GridTracker, Log4OM, and hamqsl.com), (3) archive endpoints for history graphs only. Propagation predictions also updated to use current values. N0NBH cache pre-warmed on server startup - RBN only showing CW spots — The RBN telnet parser regex required a
WPMfield, which only CW spots have. FT8, FT4, RTTY, and PSK spots were silently dropped. Fixed regex to match all spot formats by terminating atdBand optionally extracting WPM/BPS speed afterward. RBN buffer increased from 500 → 2000 spots - "fatal: couldn't find remote ref" on update (#293) — Update script and server-side git functions didn't handle broken git state: missing/wrong remote URL, detached HEAD, stale remote refs, or missing upstream tracking. Now auto-fixes remote URL, fetches with
--prune, detects and recovers detached HEAD, sets upstream tracking, and falls back togit reset --hardifgit pullfails - K-Index forecast bars not rendering — Extra wrapper
<div>withflexDirection: columnbroke the parent'salignItems: flex-endheight calculation, collapsing bar heights to zero - PSK-MQTT "Batch subscribe error" log spam — When the MQTT broker connection was unstable, each reconnect cycle logged
Batch subscribe error: Connection closedfor every attempt. Now suppresses expected "Connection closed" errors in the batch subscribe callback (same fix as v15.1.6 for individual subscribes). Also: MQTTon('error')no longer double-logs "Connection closed" alongsideon('close'); disconnect messages uselogErrorOnceto dedup; reconnect messages throttled to 1st attempt and every 5th - WSPR Heatmap double-logging 503 errors — Each 503 response logged twice: once from the fetch handler with the backoff duration, and again from the catch block. Also, changing backoff values (
36s,72s, etc.) defeatedlogErrorOncededup. Fixed: single log line per failure, stable dedup key
- Weather → client-direct Open-Meteo — Removed entire server-side weather stack (NWS, Open-Meteo proxy, background worker, throttle queue, cache). All weather is now fetched directly by each user's browser from Open-Meteo. Rate limits are distributed across all user IPs instead of concentrated on our server — eliminates the 429 backoff death spirals that plagued 2,000+ user deployments. Optional Open-Meteo API key field in Settings for users who want higher limits. Removed ~400 lines of server code
- Solar indices panel — Each section (SFI, K-Index, SSN) now shows contextual detail: condition labels (e.g. "Excellent", "Quiet", "High"), chart descriptions ("10.7cm Solar Flux — 20-day trend"), value ranges, time axis labels on K-Index bars ("Now → +24h"), and fallback explanatory text when no history data is available
- Blank screen —
filteredSatellites is not defined— DockableApp and ClassicLayout passed rawsatellites.datato WorldMap instead offilteredSatellites. The variable was never destructured from props, causing a ReferenceError that crashed the entire React tree with no error boundary to catch it. Fixed all three layouts to properly receive and passfilteredSatellites. Also means satellite filters in Settings now actually work in dockable and classic layouts - Blank screen after update — After server updates, browsers with cached old JS chunks would fail to load new modules, crashing the React app with a blank screen (users had to clear cookies/cache to fix). Three fixes: (1) global chunk-load error handler in
index.htmldetects stale module import failures and auto-reloads once; (2)update.shnow deletesdist/before rebuilding to prevent old hashed chunks from being served alongside new ones; (3) backward-compatible/api/weatherstub endpoint returns{ _direct: true }so old cached client code doesn't 404 - Global error boundary — Added
ErrorBoundarycomponent wrapping the entire app. Future render crashes show a recovery UI with "Reload Page" and "Clear Cache & Reload" buttons plus expandable error details, instead of a blank screen
- Upstream Request Manager — New
UpstreamManagerclass prevents request stampedes on external APIs. Three-layer protection: (1) in-flight request deduplication — 50 concurrent users trigger 1 upstream fetch, not 50; (2) stale-while-revalidate — serve cached data instantly while refreshing in background; (3) exponential backoff with jitter per service. Applied to PSKReporter HTTP and WSPR Heatmap endpoints - PSKReporter Server-Side MQTT Proxy — Server now maintains a single MQTT connection to
mqtt.pskreporter.infoinstead of each browser opening its own. Spots are buffered per callsign and pushed to clients via Server-Sent Events (SSE) every 10 seconds. Dynamic subscription management: subscribes when first SSE client connects for a callsign, unsubscribes 30s after last client disconnects, disconnects from broker entirely when no clients are active. Exponential backoff on broker disconnects. Health dashboard shows MQTT proxy stats (connected/callsigns/spots/clients). ClientusePSKReporterhook rewritten to useEventSourceinstead ofmqttlibrary — no more direct browser-to-broker connections - GeoIP Country Statistics — Visitor IPs resolved to country codes via ip-api.com batch endpoint (free, no API key). Results cached persistently across restarts.
/api/healthJSON includesvisitors.today.countriesandvisitors.allTime.countries(sorted by count). HTML dashboard shows "🌍 Visitor Countries" section with flag emoji badges for today and horizontal bar chart with percentages for all-time data - Weather error/retry UI — WeatherPanel now shows loading skeleton, error messages with retry countdown, and stale-data badges instead of silently disappearing when weather API is rate-limited
- WSJT-X Decode Retention Control — New time filter dropdown (5m / 15m / 30m / 60m) in the WSJT-X panel header controls how long decoded messages are kept visible in the list and on the map. Default 30 minutes, persisted in localStorage
- Weather 429 cascade — Multiple issues caused weather to disappear for all users: (1) each WeatherPanel called
useWeather()independently, doubling API calls; now fetched once at App level and passed asweatherDataprop; (2) no retry on 429 — client waited full 15-min poll; now retries at 15s→30s→60s→120s→300s; (3)WeatherPanelreturnednullon error with no feedback; now shows loading/error states - Weather overwhelmed at 2000+ users — Server was exhausting Open-Meteo's free tier (10K/day) by proxying weather for all users through a single IP. Moved weather to client-direct: each user's browser fetches from Open-Meteo directly, distributing rate limits across all user IPs. Optional API key in Settings for higher limits
- WSJT-X decodes not mapping correctly (#299) — Only 13 of 100 decodes showed map pins because: (1) only CQ messages were mapped — all QSO exchanges (signal reports, RR73, 73, grid exchanges) were filtered out even when a grid square was present; (2) grid regex
^grid$only matched if the exchange was nothing but a grid — messages likeEN82 a7(grid + signal report) failed; (3) no memory between decodes — once a station's CQ with grid scrolled off, subsequent exchanges from that callsign lost their location. Fix: map ALL decode types with resolved coordinates, extract grids from anywhere in exchange text, maintain a callsign→grid cache across decodes, and fall back to callsign prefix estimation as a last resort. Prefix-estimated locations shown at reduced opacity with (est) label in popup - PSKReporter SSE stream stuck at "Connecting" — Compression middleware was gzip-buffering SSE events; API cache middleware was setting
Cache-Controlon the stream endpoint. Fix: skip compression fortext/event-stream, skip cache headers for/stream/paths, add explicitres.flush()after every SSE write, setContent-Encoding: identityandno-transformheaders - "vite: not found" after update (#284) —
npm installskips devDependencies whenNODE_ENV=productionis set, leavingviteandvitestuninstalled. Three fixes: (1) all npm scripts now usenpx vite/npx vitestwhich auto-resolves fromnode_modules/.bin; (2)update.sh,setup-pi.sh, andsetup-linux.shnow usenpm install --include=devto force devDependency installation regardless of NODE_ENV; (3)prestartbuild step no longer runs tests —npm startjust builds and starts, tests are separate vianpm test - VOACAP heatmap blocks DX click — Heatmap grid rectangles had
interactive: truewith popup bindings, which consumed map clicks before they could reach the DX-setting handler. Set tointeractive: falseso clicks pass through. The color-coded grid with legend still communicates propagation reliability visually - README/docs cleanup — Corrected OpenWeatherMap description (only needed for cloud layer overlay, not weather data). Added "Can't find
.env?" guidance box with instructions for showing hidden files on Linux/Pi/Mac. Added FAQ entry about.envlocation. Weather data sources section updated to reflect client-direct Open-Meteo architecture - PSK-MQTT "Connection closed" subscribe spam — When the MQTT broker connection dropped, a race condition caused
pskMqtt.connectedto still betruewhile the socket was dead. Incoming SSE clients would callsubscribeCallsign(), which passed the connected check but got "Connection closed" callbacks — one error per callsign, flooding the log with 40+ lines. Fix: suppress expected "Connection closed" errors (reconnect handler re-subscribes all callsigns anyway), and batch all reconnect subscriptions into a single MQTT subscribe call instead of individual calls per callsign
- PSKReporter HTTP backfill — Removed the
/api/pskreporter/http/:callsignendpoint and all client-sidefetchHistorical()code. With 2,000+ concurrent users, every new SSE connection triggered 2 HTTP requests to PSKReporter's retrieve API (TX + RX), causing constant 503 errors and backoff. The backoff was shared with the WSPR heatmap endpoint, so PSK failures were taking WSPR down too. The SSE connected event already delivers up to 500 recent spots from the server's MQTT buffer — no HTTP backfill needed. Net effect: zero HTTP requests to PSKReporter for live spot data, cleaner upstream status on health dashboard - WSPR Heatmap had zero backoff — PSKReporter 503 responses were ignored; WSPR kept hammering on every 2-min poll. Now shares PSKReporter's exponential backoff via UpstreamManager
- WSJT-X decode limits — Server buffer: 200 → 500 decodes. Max age: 30 → 60 minutes. Client ring buffer: 200 → 500. These are the raw limits; the new retention dropdown (5m/15m/30m/60m) controls what the user actually sees
- WSPR client polling — 2 min → 5 min (server caches for 10 min anyway)
- PSKReporter backoff — Replaced fixed-duration backoff (15 min / 1 hr) with exponential backoff: 30s → 60s → 120s → ... capped at 30 min, with 0-15s random jitter to prevent synchronized retry storms
- VOACAP Propagation Heatmap — New map layer plugin (
voacap-heatmap) overlays color-coded propagation predictions across the globe for a selected band. Draggable/minimizable control panel with band selector (160m–6m), grid resolution (5°–20°), and color legend. Server-side/api/propagation/heatmapendpoint computes reliability grid using ITU-R P.533-style model with live solar indices. 5-minute server cache, 3 world copies for dateline support, click popups with reliability %, distance, and grid coordinates - Propagation Mode & Power — VOACAP predictions now factor in operating mode and TX power. Eight modes supported (SSB, CW, FT8, FT4, WSPR, JS8, RTTY, PSK31) with physically-modeled decode advantages (+34dB for FT8, +41dB for WSPR vs SSB baseline). Power offset in dB relative to 100W. Signal margin widens/narrows effective MUF/LUF window — FT8 shows bands "open" that SSB shows "closed". Configurable in Settings → Station tab with preset power buttons (5W/25W/100W/1.5kW) + custom watt input. Live margin readout. Applied to both main propagation panel and VOACAP heatmap map layer
- Distance Units — Global metric/imperial toggle in Settings. Affects all distance displays: DE↔DX distance (LocationPanel), propagation path distance, ionosonde distance, satellite altitude & range, great circle path popup, WSPR spot distances & efficiency, VOACAP heatmap cell popups. Default: Imperial (mi)
- Custom Terminator — Replaced CDN-based
L.terminatorwith built-insrc/utils/terminator.jsimplementation that spans 3 world copies for seamless dateline crossing
- Gray line disappearing past dateline — Replaced
splitAtDateLine()withunwrapAndCopyLine()/unwrapAndCopyPolygon()in gray line plugin. All 5 render paths fixed (main terminator, enhanced DX zone, civil/nautical/astronomical twilight) - Sun/moon marker updates — Now update every 60 seconds instead of only on initial render
- DX Cluster frequency format (Classic/Tablet/Compact) — Frequencies showed
14.1instead of14.070in non-Modern layouts. Fixed.toFixed(1)→.toFixed(3)and added kHz→MHz conversion for all 3 ClassicLayout DX cluster displays
- Per-panel font sizing (Dockable Mode) — A−/A+ buttons in each panel's tabset header. 10 zoom steps from 70% to 200%, persisted per-panel in localStorage. Percentage badge shown when zoomed; click to reset. World Map excluded (has its own zoom)
- DX News Ticker toggle — New checkbox in Settings → Map Layers tab to show/hide the scrolling DX news ticker. Persisted in localStorage with other map layer settings
- Weather proxy — New
/api/weatherserver endpoint proxies Open-Meteo requests. Coordinates rounded to ~11km grid for cache sharing across users. 15-minute cache, 1-hour stale serving on rate limit/errors. Client debounced (2s) to prevent rapid-fire calls when clicking through DX spots
- ITU-R P.533 by default — All installs now use the public OpenHamClock ITURHFProp service (
proppy-production.up.railway.app) for propagation predictions out of the box. No.envconfiguration needed. Self-hosting still supported viaITURHFPROP_URLoverride
- DX Cluster spot clicks — Clicking a DX cluster spot now updates the DX panel and map. Root cause:
DXClusterPanelhad noonClickhandler; paths data with coordinates wasn't being looked up. Fixed across Modern, Classic, and Dockable layouts - RBN layer showing N0CALL — RBN (and all plugin layers) showed "N0CALL" instead of the user's callsign. Root cause:
WorldMapwasn't passingcallsign,locator, orlowMemoryModetoPluginLayer. Also fixed 4 of 6WorldMapinstances across layouts that were missing thecallsignprop entirely - Update button fails with "Local changes detected" —
git status --porcelainblocked updates when file permissions changed (e.g.,chmod +x update.sh) or on cross-platform mode differences. Fix:git config core.fileMode falseset at server startup, in setup scripts, and inupdate.sh. Auto-update now stashes local changes before pulling instead of refusing - Update button missing in Dockable Mode —
DockableAppwasn't passingonUpdateClick,updateInProgress, orshowUpdateButtonto the Header component - PSKReporter missing spots — Only showed spots received after page load (MQTT-only, no history). Now fetches historical spots via
/api/pskreporter/http/:callsignon connect, then merges with real-time MQTT stream. Also: time window increased from 15 to 30 minutes, max spots increased from 100 to 500 (50 in low-memory mode), deduplication changed from freq-based (dropped legitimate spots) to callsign+band keyed (keeps most recent per station per band), server-side report cap raised from 100 to 500 - Update script "fatal: couldn't find remote ref master" — The
main||masterfallback pattern rangit pull origin mastereven aftergit pull origin mainsucceeded (non-zero exit from suppressed warnings). Script now detects the correct branch once at startup. Same fix applied to server-side auto-update - Stale browser cache after updates —
index.htmlwas cached for 1 day (maxAge: '1d'), causing browsers to load old JavaScript bundles after a local update. New features (like toggles) wouldn't appear until cache expired. Fix:index.htmlnow served withno-cache, no-store, must-revalidateheaders. Hashed JS/CSS assets still cached for 1 year (filenames change on rebuild) - WSJT-X relay agent ECONNRESET — Relay v1.1.0: added
Connection: closeheader, startup connectivity test, clear error diagnostics for ECONNRESET/ECONNREFUSED/DNS/timeout - Pi kiosk mode loses settings on reboot — Chromium
--incognitoflag wiped localStorage on every restart. Replaced with dedicated--user-data-dirprofile.update.shauto-patches existing kiosk installs - Open-Meteo 429 rate limiting — Client-side Open-Meteo calls replaced with server-side proxy (see Weather proxy above)
- Map jumping near dateline (Australia/NZ/Pacific) — Panning east or west past 180° longitude caused the map to snap violently. Root cause:
moveendhandler normalized center longitude to ±180°, fighting Leaflet'sworldCopyJump. Also: tile layerboundsrestricted to [-180, 180] prevented tiles from loading in world copies. Fix: center longitude no longer normalized (Leaflet manages wrap internally), tile bounds removed for all styles except MODIS (which only covers -180..180)
- N0NBH Band Conditions — Real-time band condition data from N0NBH's NOAA-sourced feed replaces the old calculated estimates. Server-side
/api/n0nbhendpoint with 1-hour caching. Day/night conditions per band, VHF conditions (Aurora, E-skip by region), geomagnetic field status, signal noise level, and MUF. PropagationPanel shows mini day/night indicators when conditions differ between day and night - User Profiles — Save and load named configuration profiles from Settings → Profiles tab. Each profile snapshots all localStorage keys (config, layout, filters, map layers, preferences). Supports save, load, rename, delete, export to JSON file, and import from file. Useful for multi-operator shared stations or switching between personal views (contest mode, field day, everyday)
- Concurrent User Tracking — Health dashboard (
/api/health) now shows real-time concurrent users, peak concurrent count, session duration analytics (avg/median/p90/max), duration distribution buckets, and an active users table with anonymized IPs and session durations - Auto-Refresh on Update — New
useVersionCheckhook polls/api/versionevery 60 seconds. When a new version is detected after deployment, connected browsers show a toast notification and automatically reload after 3 seconds. Lightweight/api/versionendpoint with no-cache headers - Cloud Layer Restriction — OWM cloud overlay restricted to local installs only via
localOnlyflag in layer registry. Cloud layer invisible on openhamclock.com, visible on localhost/LAN - A-Index Display — A-index and geomagnetic field status added to Header and ClassicLayout solar stats bars, color-coded by severity
- Space Weather Extras — Header shows A-index (color-coded: green <10, amber 10-19, red ≥20) and geomagnetic field status from N0NBH data
- Band Conditions Rewrite —
useBandConditionshook completely rewritten. Removed 200+ lines of local SFI/K-index formula calculations. Now fetches from/api/n0nbhserver proxy and maps N0NBH grouped ranges (80m-40m, 30m-20m, etc.) to individual bands - Health Dashboard Auto-Refresh — HTML health dashboard now auto-refreshes every 30 seconds
- Stats Grid — Health dashboard shows 6 stat cards (added Online Now and Peak Concurrent)
- Donate Buttons — Hidden in fullscreen mode across Header, ModernLayout, and ClassicLayout
- CI Pipeline — Dropped Node 18 (replaced with 20.x/22.x), replaced
npm startwithnode server.jsto skip redundant prestart build, added retry loop for health check (up to 30 attempts), same retry pattern for Docker health check - Version — Bumped to 15.0.0
- CI Health Check Failure —
npm startwas runningprestart(full rebuild) before starting the server, causing the 5-secondsleep+curlto fail every time. Now usesnode server.jsdirectly since the build step already ran
- State persistence — All user preferences survive page refresh: PSK/WSJT-X panel mode, TX/RX tab, solar image wavelength, weather panel expanded state, temperature unit
- Collapsible weather — DE location weather section collapses to one-line summary, expands for full details
- Lunar phase display — 4th cycling mode in Solar panel shows current moon phase with SVG rendering, illumination %, and next full/new moon dates
- F°/C° toggle — Switch temperature units with localStorage persistence; header always shows both
- Satellite filtering — Complete satellite filter interface in Settings → Satellites tab. Select/deselect from 40+ satellites, real-time visibility status, persistent filters
- WSPR heatmap improvements — Increased brightness (opacity 0.75-1.0), 4-layer glow effect, tighter clustering (radius 50,000m → 6,000m), adjustable opacity slider
- DX Target enhancements — Distance calculation (Haversine), beam headings (SP/LP), color-coded display
- Lightning detection — WebSocket server fallback system, proximity alerts, RBN history management
- WSPR data quality — Spot limit increased from 2,000 to 10,000, detailed marker tooltips with power/SNR/distance/efficiency
- PSKReporter MQTT — Field mapping used
sa/ra(ADIF country codes) instead ofsc/rc(callsigns), so no MQTT spots ever matched - PSKReporter RX topic — Subscription pattern had one extra wildcard
- PSKReporter HTTP fallback — If MQTT fails within 12 seconds, automatically falls back to HTTP API
- Map layer persistence — Map style/zoom save was overwriting plugin layer settings. Now merges correctly
- Version consistency — All version numbers now read from package.json as single source of truth
- PSKReporter 403 spam — Server backs off for 30 minutes on 403/429 responses
- WSPR heatmap infinite loop — Removed heatmapLayer from useEffect dependencies
- WSPR grid filter — Supports 2-6 character grids, prefix matching (FN → FN03, FN21)
- WSPR callsign filter — Proper suffix stripping (VE3TOS/M → VE3TOS), respects grid filter state
- Satellite initialization — Fixed ReferenceError when filteredSatellites referenced satellites.data before hook initialized
- VOACAP ionosonde label — Added "Iono:" prefix to clarify it's the data source, not the DX location
- WSPR update frequency — Polling interval from 5 minutes to 60 seconds
- WSPR band chart — Removed pulsing animation, added smooth CSS transition
- WSPR MQTT — Real-time MQTT feed attempted and reverted due to mixed content policy (HTTPS pages cannot connect to insecure WebSocket)
- PSKReporter Integration — New panel showing stations hearing you (TX) and stations you're hearing (RX). Supports FT8, FT4, JS8, and other digital modes. Configurable time window. Signal paths drawn on map
- Bandwidth Optimization — Reduced network egress by ~85%: GZIP compression, server-side caching, reduced polling intervals, HTTP Cache-Control headers
- Empty ITURHFPROP_URL causing "Only absolute URLs supported" error
- Satellite TLE fetch timeout errors handled silently
- Reduced console log spam for network errors
- Environment-based configuration —
.envfile auto-created from.env.exampleon first run. Supports CALLSIGN, LOCATOR, PORT, HOST, UNITS, TIME_FORMAT, THEME, LAYOUT - Auto-build on start —
npm startautomatically builds React frontend - Update script —
./scripts/update.shfor easy local/Pi updates - Network access configuration —
HOST=0.0.0.0for LAN access - Grid locator auto-conversion — Calculates lat/lon from LOCATOR
- Setup wizard — Settings panel auto-opens if callsign or locator missing
- Retro theme — 90s Windows style
- Classic layout — Original HamClock-style with black background and large colored numbers
- Configuration priority: localStorage > .env > defaults
- DX Spider connection uses dxspider.co.uk as primary
- Header clock "shaking" when digits change
- Header layout wrapping on smaller screens
- Reduced log spam with rate-limited error logging
- 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
- DX Filter modal crash when opening
- K-Index display showing correct values
- Contest calendar attribution
- Multiple DX cluster source fallbacks
- ITURHFProp hybrid propagation predictions
- Ionosonde real-time corrections
- Modular React architecture with Vite
- 13 extracted components, 12 custom hooks, 3 utility modules
- Railway deployment support
- Docker support
- Complete rewrite from monolithic HTML to modular React
- Initial modular extraction from monolithic codebase
- React + Vite build system
- Express backend for API proxying
- Three themes: Dark, Light, Legacy
- 15.x — N0NBH band conditions, user profiles, concurrent user tracking, auto-refresh, CI fixes
- 3.12.x — PSKReporter fixes, state persistence, satellite filtering, WSPR improvements, lunar phase
- 3.11.x — PSKReporter integration, bandwidth optimization
- 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