From 70f22795545cf4f5279862fa0dc23400b5e44fc1 Mon Sep 17 00:00:00 2001 From: accius Date: Thu, 29 Jan 2026 23:44:18 -0500 Subject: [PATCH] initial commit v3 --- .github/ISSUE_TEMPLATE/bug_report.md | 48 ++ .github/ISSUE_TEMPLATE/feature_request.md | 41 + .github/workflows/ci.yml | 86 ++ .gitignore | 81 ++ CHANGELOG.md | 92 +++ CODE_OF_CONDUCT.md | 44 + CONTRIBUTING.md | 219 +++++ Dockerfile | 54 ++ LICENSE | 27 + README.md | 387 +++++---- docker-compose.yml | 24 + electron/main.js | 287 +++++++ package.json | 96 +++ public/index.html | 929 ++++++++++++++++++++++ railway.toml | 13 + scripts/setup-linux.sh | 125 +++ scripts/setup-pi.sh | 355 +++++++++ scripts/setup-windows.ps1 | 127 +++ server.js | 235 ++++++ 19 files changed, 3100 insertions(+), 170 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 docker-compose.yml create mode 100644 electron/main.js create mode 100644 package.json create mode 100644 public/index.html create mode 100644 railway.toml create mode 100644 scripts/setup-linux.sh create mode 100644 scripts/setup-pi.sh create mode 100644 scripts/setup-windows.ps1 create mode 100644 server.js diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b4c1a8e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,48 @@ +--- +name: Bug Report +about: Create a report to help us improve OpenHamClock +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Describe the Bug +A clear and concise description of what the bug is. + +## To Reproduce +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +## Expected Behavior +A clear and concise description of what you expected to happen. + +## Screenshots +If applicable, add screenshots to help explain your problem. + +## Environment +- **OS**: [e.g., Windows 11, macOS Sonoma, Ubuntu 22.04, Raspberry Pi OS] +- **Browser**: [e.g., Chrome 120, Firefox 121, Safari 17] +- **Node.js Version**: [e.g., 20.10.0] +- **OpenHamClock Version**: [e.g., 3.0.0] +- **Running As**: [Web browser / Electron app / Docker] + +## For Raspberry Pi Users +- **Pi Model**: [e.g., Pi 4 4GB, Pi 3B+] +- **Pi OS Version**: [e.g., Bookworm 64-bit] +- **Display**: [e.g., Official 7" touchscreen, HDMI 1080p monitor] +- **Running in Kiosk Mode**: [Yes/No] + +## Console Errors +If there are any errors in the browser console (F12), please paste them here: +``` +Paste console errors here +``` + +## Additional Context +Add any other context about the problem here. Include your callsign if you're comfortable sharing it! + +--- +73! diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..b4bf113 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,41 @@ +--- +name: Feature Request +about: Suggest an idea for OpenHamClock +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Is your feature request related to a problem? +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +## Describe the Solution You'd Like +A clear and concise description of what you want to happen. + +## Describe Alternatives You've Considered +A clear and concise description of any alternative solutions or features you've considered. + +## Use Case +How would this feature benefit amateur radio operators? Be specific about the use case: +- [ ] General HF operation +- [ ] VHF/UHF operation +- [ ] Satellite operation +- [ ] Contesting +- [ ] POTA/SOTA +- [ ] DXing +- [ ] Emergency communications +- [ ] Other: ___________ + +## Would you be willing to help implement this? +- [ ] Yes, I can submit a PR +- [ ] Yes, I can help test +- [ ] No, but I'd use it! + +## Additional Context +Add any other context, screenshots, or mockups about the feature request here. + +## Similar Features in Other Software +Are there similar features in other amateur radio software? If so, what do you like/dislike about their implementation? + +--- +73! diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..63165ca --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Start server and test health endpoint + run: | + npm start & + sleep 5 + curl -f http://localhost:3000/api/health || exit 1 + + docker: + runs-on: ubuntu-latest + needs: test + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t openhamclock:test . + + - name: Test Docker container + run: | + docker run -d -p 3000:3000 --name ohc-test openhamclock:test + sleep 10 + curl -f http://localhost:3000/api/health || exit 1 + docker stop ohc-test + + build-electron: + runs-on: ${{ matrix.os }} + needs: test + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build Electron app + run: npm run electron:build + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: electron-${{ matrix.os }} + path: dist/ + retention-days: 7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03e2829 --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +# Dependencies +node_modules/ +package-lock.json + +# Build outputs +dist/ +build/ +out/ + +# Electron +*.asar + +# Logs +logs/ +*.log +npm-debug.log* + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory +coverage/ +.nyc_output/ + +# Environment files +.env +.env.local +.env.*.local +*.env + +# Editor directories and files +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.classpath +.settings/ +*.sublime-workspace +*.sublime-project + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +Desktop.ini + +# Temporary files +tmp/ +temp/ +*.tmp +*.temp + +# Config files that might contain secrets +config.local.js +config.local.json + +# Test files +*.test.js.snap + +# Optional npm cache directory +.npm/ + +# Optional eslint cache +.eslintcache + +# Yarn +.yarn/ +.pnp.* + +# Raspberry Pi specific +*.img +*.iso diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..95050a7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,92 @@ +# Changelog + +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 +- Satellite tracking with pass predictions +- SOTA API integration +- Contest calendar +- WebSocket DX cluster connection +- Azimuthal equidistant projection option + +## [3.0.0] - 2024-01-30 + +### 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 + +### 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 + +## [2.0.0] - 2024-01-29 + +### 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) + +### Changed +- Improved space weather display with color coding +- Better visual hierarchy in panels + +## [1.0.0] - 2024-01-29 + +### 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 + +--- + +## Version History Summary + +| Version | Date | Highlights | +|---------|------|------------| +| 3.0.0 | 2024-01-30 | Real maps, Electron, Docker, Railway | +| 2.0.0 | 2024-01-29 | Live APIs, improved map | +| 1.0.0 | 2024-01-29 | Initial release | + +--- + +*73 de OpenHamClock contributors* diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..525a571 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,44 @@ +# Code of Conduct + +## Our Pledge + +In the spirit of amateur radio's long tradition of courtesy, we as contributors and maintainers pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members +* Following amateur radio's tradition of courtesy and helpfulness + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information without explicit permission +* Other conduct which could reasonably be considered inappropriate + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team through GitHub issues. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4. + +--- + +**73 - The amateur radio community is built on mutual respect and helpfulness. Let's keep that tradition alive!** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c946159 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,219 @@ +# Contributing to OpenHamClock + +First off, thank you for considering contributing to OpenHamClock! It's people like you that make the amateur radio community great. 73! + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [How Can I Contribute?](#how-can-i-contribute) +- [Development Setup](#development-setup) +- [Pull Request Process](#pull-request-process) +- [Style Guidelines](#style-guidelines) + +## Code of Conduct + +This project and everyone participating in it is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers. + +## Getting Started + +### Issues + +- **Bug Reports**: If you find a bug, please create an issue with a clear title and description. Include as much relevant information as possible, including steps to reproduce. +- **Feature Requests**: We welcome feature suggestions! Open an issue describing the feature and why it would be useful. +- **Questions**: Use GitHub Discussions for questions about usage or development. + +### Good First Issues + +Looking for something to work on? Check out issues labeled [`good first issue`](https://github.com/k0cjh/openhamclock/labels/good%20first%20issue) - these are great for newcomers! + +## How Can I Contribute? + +### Reporting Bugs + +Before creating a bug report, please check existing issues to avoid duplicates. When you create a bug report, include: + +- **Clear title** describing the issue +- **Steps to reproduce** the behavior +- **Expected behavior** vs **actual behavior** +- **Screenshots** if applicable +- **Environment details**: OS, browser, Node.js version, Pi model, etc. + +### Suggesting Features + +We love hearing ideas from the community! When suggesting a feature: + +- **Use a clear title** for the issue +- **Provide a detailed description** of the proposed feature +- **Explain the use case** - how would this benefit ham radio operators? +- **Consider implementation** - any ideas on how to build it? + +### Priority Contribution Areas + +We especially welcome contributions in these areas: + +1. **Satellite Tracking** + - TLE parsing and SGP4 propagation + - Pass predictions and AOS/LOS times + - Satellite footprint visualization + +2. **Real-time DX Cluster** + - WebSocket connection to Telnet clusters + - Spot filtering and alerting + - Clickable spots to set DX + +3. **Contest Integration** + - Contest calendar from WA7BNM or similar + - Contest-specific band plans + - Rate/multiplier tracking + +4. **Hardware Integration** + - Hamlib radio control (frequency, mode) + - Rotator control + - External GPIO for Pi (PTT, etc.) + +5. **Accessibility** + - Screen reader support + - High contrast themes + - Keyboard navigation + +6. **Internationalization** + - Translation framework + - Localized date/time formats + - Multi-language support + +## Development Setup + +### Prerequisites + +- Node.js 18 or later +- Git +- A modern web browser + +### Local Development + +```bash +# Clone your fork +git clone https://github.com/YOUR_USERNAME/openhamclock.git +cd openhamclock + +# Add upstream remote +git remote add upstream https://github.com/k0cjh/openhamclock.git + +# Install dependencies +npm install + +# Start development server +npm run dev + +# In another terminal, run Electron (optional) +npm run electron +``` + +### Project Structure + +``` +openhamclock/ +├── public/index.html # Main application (React + Leaflet) +├── server.js # Express API proxy server +├── electron/main.js # Desktop app wrapper +├── scripts/ # Platform setup scripts +└── package.json # Dependencies and scripts +``` + +### Making Changes + +1. Create a new branch from `main`: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. Make your changes + +3. Test thoroughly: + - Test in multiple browsers (Chrome, Firefox, Safari) + - Test on desktop and mobile viewports + - Test the Electron app if applicable + - Verify API proxy endpoints work + +4. Commit with clear messages: + ```bash + git commit -m "Add satellite tracking panel with TLE parser" + ``` + +## Pull Request Process + +1. **Update documentation** if needed (README, inline comments) + +2. **Ensure your code follows style guidelines** (see below) + +3. **Test your changes** on multiple platforms if possible + +4. **Create the Pull Request**: + - Use a clear, descriptive title + - Reference any related issues (`Fixes #123`) + - Describe what changes you made and why + - Include screenshots for UI changes + +5. **Respond to feedback** - maintainers may request changes + +6. **Once approved**, a maintainer will merge your PR + +### PR Title Format + +Use conventional commit style: +- `feat: Add satellite tracking panel` +- `fix: Correct timezone calculation for DST` +- `docs: Update Pi installation instructions` +- `style: Improve mobile responsive layout` +- `refactor: Simplify API proxy endpoints` + +## Style Guidelines + +### JavaScript + +- Use modern ES6+ syntax +- Prefer `const` over `let`, avoid `var` +- Use meaningful variable and function names +- Add comments for complex logic +- Keep functions focused and small + +### CSS + +- Use CSS custom properties (variables) for theming +- Follow the existing naming conventions +- Prefer flexbox/grid over floats +- Test responsive breakpoints + +### React Components + +- Use functional components with hooks +- Keep components focused on single responsibilities +- Extract reusable logic into custom hooks +- Use meaningful prop names + +### Git Commits + +- Write clear, concise commit messages +- Use present tense ("Add feature" not "Added feature") +- Reference issues when applicable + +## Recognition + +Contributors will be recognized in: +- The README contributors section +- Release notes for significant contributions +- The project's GitHub contributors page + +## Questions? + +Feel free to: +- Open a GitHub Discussion +- Reach out to maintainers +- Join the amateur radio community discussions + +--- + +**73 and thanks for contributing to OpenHamClock!** + +*In memory of Elwood Downey, WB0OEW* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ff2edf5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +# OpenHamClock Dockerfile +# Multi-stage build for optimized production image + +# ============================================ +# Stage 1: Build +# ============================================ +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies (including dev for build) +RUN npm ci --only=production + +# ============================================ +# Stage 2: Production +# ============================================ +FROM node:20-alpine AS production + +# Set environment +ENV NODE_ENV=production +ENV PORT=3000 + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S openhamclock -u 1001 + +WORKDIR /app + +# Copy node_modules from builder +COPY --from=builder /app/node_modules ./node_modules + +# Copy application files +COPY package*.json ./ +COPY server.js ./ +COPY public ./public + +# Set ownership +RUN chown -R openhamclock:nodejs /app + +# Switch to non-root user +USER openhamclock + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 + +# Start server +CMD ["node", "server.js"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7830205 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +MIT License + +Copyright (c) 2024-2026 OpenHamClock Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +This project is dedicated to the memory of Elwood Downey, WB0OEW, creator of +the original HamClock. His contributions to the amateur radio community will +never be forgotten. 73, OM. diff --git a/README.md b/README.md index 4d0dba5..8bdb8ed 100644 --- a/README.md +++ b/README.md @@ -1,242 +1,289 @@ -# OpenHamClock +# 🌐 OpenHamClock -**A modern, open-source amateur radio dashboard - spiritual successor to HamClock** +
-*In memory of Elwood Downey, WB0OEW, creator of the original HamClock* +![OpenHamClock Banner](https://img.shields.io/badge/OpenHamClock-v3.0.0-orange?style=for-the-badge) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](LICENSE) +[![Node.js](https://img.shields.io/badge/Node.js-18+-brightgreen?style=for-the-badge&logo=node.js)](https://nodejs.org/) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge)](CONTRIBUTING.md) ---- +**A modern, open-source amateur radio dashboard with real-time space weather, band conditions, DX cluster, and interactive world maps.** -## Overview +*In loving memory of Elwood Downey, WB0OEW, creator of the original HamClock* -OpenHamClock is a web-based kiosk-style application that provides real-time space weather, radio propagation information, and other data useful to amateur radio operators. It's designed to run on any platform with a web browser, with special consideration for Raspberry Pi deployments. +[**Live Demo**](https://openhamclock.up.railway.app) · [**Download**](#-installation) · [**Documentation**](#-features) · [**Contributing**](#-contributing) -## Features +![OpenHamClock Screenshot](https://via.placeholder.com/800x450/0a0e14/ffb432?text=OpenHamClock+Screenshot) -### Current Features (v1.0.0) +
-- **World Map with Day/Night Terminator** - - Real-time gray line display - - Sun position tracking - - DE and DX location markers with path visualization +--- -- **Time Displays** - - UTC time (large, prominent display) - - Local time with date - - Uptime counter +## 📡 About -- **Location Information** - - DE (your location) with Maidenhead grid square - - DX (target location) with grid square - - Short path and long path bearing - - Distance calculation - - Sunrise/sunset times for both locations +OpenHamClock is a spiritual successor to the beloved HamClock application created by Elwood Downey, WB0OEW. After Elwood's passing and the announcement that HamClock will cease functioning in June 2026, the amateur radio community came together to create an open-source alternative that carries forward his vision. -- **Space Weather Panel** - - Solar Flux Index (SFI) - - Sunspot Number - - K-Index and A-Index - - X-Ray flux - - Overall conditions assessment +### Why OpenHamClock? -- **Band Conditions** - - Visual display for all HF bands - - Color-coded conditions (Good/Fair/Poor) - - VHF band status +- **Open Source**: MIT licensed, community-driven development +- **Cross-Platform**: Runs on Windows, macOS, Linux, and Raspberry Pi +- **Modern Stack**: Built with web technologies for easy customization +- **Real Maps**: Actual satellite/terrain imagery, not approximations +- **Live Data**: Real-time feeds from NOAA, POTA, SOTA, and DX clusters +- **Self-Hosted**: Run locally or deploy to your own server -- **DX Cluster Feed** - - Live spot display (placeholder for API integration) - - Frequency, callsign, comment, and time +--- -- **POTA Activity** - - Parks on the Air activator tracking - - Reference, frequency, and mode display +## ✨ Features + +### 🗺️ Interactive World Map +- **8 map styles**: Dark, Satellite, Terrain, Streets, Topo, Ocean, NatGeo, Gray +- **Real-time day/night terminator** (gray line) +- **Great circle paths** between DE and DX +- **Click anywhere** to set DX location +- **POTA activators** displayed on map +- **Zoom and pan** with full interactivity + +### 📊 Live Data Integration + +| Source | Data | Update Rate | +|--------|------|-------------| +| NOAA SWPC | Solar Flux, K-Index, Sunspots | 5 min | +| POTA | Parks on the Air spots | 1 min | +| SOTA | Summits on the Air spots | 1 min | +| DX Cluster | Real-time DX spots | 30 sec | +| HamQSL | Band conditions | 5 min | + +### 🕐 Station Information +- **UTC and Local time** with date +- **Maidenhead grid square** (6 character) +- **Sunrise/Sunset times** for DE and DX +- **Short path/Long path bearings** +- **Great circle distance** calculation +- **Space weather conditions** assessment + +### 📻 Band Conditions +- Visual display for 160m through 70cm +- Color-coded: Good (green), Fair (amber), Poor (red) +- Based on real propagation data -### Planned Features (Roadmap) +--- -- [ ] Live API integration for space weather (NOAA, hamqsl.com) -- [ ] Real DX cluster connectivity (Telnet/WebSocket) -- [ ] Live POTA/SOTA API integration -- [ ] Satellite tracking -- [ ] VOACAP propagation predictions -- [ ] Contest calendar integration -- [ ] Hamlib/flrig radio control -- [ ] Rotator control -- [ ] Customizable panel layout -- [ ] Multiple map projections -- [ ] ADIF log file integration -- [ ] RESTful API for external control -- [ ] Touch screen support -- [ ] Alarm/alert system +## 🚀 Installation -## Installation +### Quick Start (Any Platform) -### Option 1: Direct Browser Use +```bash +# Clone the repository +git clone https://github.com/k0cjh/openhamclock.git +cd openhamclock -Simply open `index.html` in any modern web browser. No server required! +# Install dependencies +npm install -```bash -# Clone or download the files -firefox index.html -# or -chromium-browser index.html +# Start the server +npm start + +# Open http://localhost:3000 in your browser ``` -### Option 2: Raspberry Pi Kiosk Mode +### One-Line Install -1. **Install Raspberry Pi OS** (Desktop version recommended) +**Linux/macOS:** +```bash +curl -fsSL https://raw.githubusercontent.com/k0cjh/openhamclock/main/scripts/setup-linux.sh | bash +``` -2. **Copy OpenHamClock files** - ```bash - mkdir ~/openhamclock - cp index.html ~/openhamclock/ - ``` +**Windows (PowerShell as Admin):** +```powershell +Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/k0cjh/openhamclock/main/scripts/setup-windows.ps1')) +``` -3. **Run the setup script** - ```bash - chmod +x setup-pi.sh - ./setup-pi.sh - ``` +### 🍓 Raspberry Pi -4. **Manual Kiosk Setup** (alternative) - ```bash - # Install unclutter to hide mouse cursor - sudo apt-get install unclutter +```bash +# Download and run the Pi setup script +curl -fsSL https://raw.githubusercontent.com/k0cjh/openhamclock/main/scripts/setup-pi.sh -o setup-pi.sh +chmod +x setup-pi.sh + +# Standard installation +./setup-pi.sh - # Create autostart entry - mkdir -p ~/.config/autostart - cat > ~/.config/autostart/openhamclock.desktop << EOF - [Desktop Entry] - Type=Application - Name=OpenHamClock - Exec=chromium-browser --kiosk --noerrdialogs --disable-infobars --incognito file:///home/pi/openhamclock/index.html - EOF - ``` +# Or with kiosk mode (fullscreen, auto-start on boot) +./setup-pi.sh --kiosk +``` -### Option 3: Local Web Server +**Supported Pi Models:** +- Raspberry Pi 3B / 3B+ ✓ +- Raspberry Pi 4 (2GB+) ✓✓ (Recommended) +- Raspberry Pi 5 ✓✓✓ (Best performance) -For advanced features (future API integrations), run with a local server: +### 🖥️ Desktop App (Electron) ```bash -# Python 3 -cd openhamclock -python3 -m http.server 8080 +# Development +npm run electron + +# Build for your platform +npm run electron:build -# Then open http://localhost:8080 +# Build for specific platform +npm run electron:build:win # Windows +npm run electron:build:mac # macOS +npm run electron:build:linux # Linux ``` -### Option 4: Electron Desktop App (Future) +### 🐳 Docker -Coming soon: Packaged desktop applications for Windows, macOS, and Linux. +```bash +# Build the image +docker build -t openhamclock . -## Configuration +# Run the container +docker run -p 3000:3000 openhamclock -Edit the following values in `index.html` to customize: +# Or use Docker Compose +docker compose up -d +``` -```javascript -// Your callsign -const [callsign, setCallsign] = useState('YOUR_CALL'); +### ☁️ Deploy to Railway -// Your location (lat, lon) -const [deLocation, setDeLocation] = useState({ lat: 39.7392, lon: -104.9903 }); +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/openhamclock) -// Default DX location -const [dxLocation, setDxLocation] = useState({ lat: 35.6762, lon: 139.6503 }); -``` +Or manually: +1. Fork this repository +2. Create a new project on [Railway](https://railway.app) +3. Connect your GitHub repository +4. Deploy! -### Future Configuration File +--- -A separate `config.js` will be provided for easier configuration: +## ⚙️ Configuration + +Edit your callsign and location in `public/index.html`: ```javascript -// config.js (coming soon) -export default { - callsign: 'K0CJH', - location: { - lat: 39.7392, - lon: -104.9903 - }, - theme: 'dark', - panels: ['clock', 'map', 'weather', 'dx', 'bands'], - // ... more options +const CONFIG = { + callsign: 'YOUR_CALL', + location: { lat: YOUR_LAT, lon: YOUR_LON }, + defaultDX: { lat: 35.6762, lon: 139.6503 }, + // ... }; ``` -## Display Resolutions +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `PORT` | `3000` | Server port | +| `NODE_ENV` | `development` | Environment mode | + +--- + +## 🗺️ Map Styles -OpenHamClock is responsive and works at various resolutions: +| Style | Provider | Best For | +|-------|----------|----------| +| **Dark** | CartoDB | Night use, low-light shacks | +| **Satellite** | ESRI | Terrain visualization | +| **Terrain** | OpenTopoMap | SOTA operations | +| **Streets** | OpenStreetMap | Urban navigation | +| **Topo** | ESRI | Detailed terrain | +| **Ocean** | ESRI | Maritime operations | +| **NatGeo** | ESRI | Classic cartography | +| **Gray** | ESRI | Minimal, distraction-free | + +--- + +## 🛠️ Development + +```bash +# Clone and setup +git clone https://github.com/k0cjh/openhamclock.git +cd openhamclock +npm install -| Resolution | Recommended Use | -|------------|-----------------| -| 800x480 | Small Pi displays, Inovato Quadra | -| 1024x600 | 7" Pi touchscreens | -| 1280x720 | HD ready monitors | -| 1600x960 | Recommended for full features | -| 1920x1080 | Full HD monitors | -| 2560x1440 | Large displays, high detail | +# Start development server +npm run dev -## API Data Sources (Planned) +# Run Electron in dev mode +npm run electron +``` -| Data | Source | Status | -|------|--------|--------| -| Space Weather | NOAA SWPC | Planned | -| Band Conditions | hamqsl.com | Planned | -| DX Cluster | Various Telnet nodes | Planned | -| POTA | pota.app API | Planned | -| SOTA | sotawatch.org | Planned | -| Satellites | N2YO, CelesTrak | Planned | +### Project Structure -## Technical Details +``` +openhamclock/ +├── public/ # Static web files +│ ├── index.html # Main application +│ └── icons/ # App icons +├── electron/ # Electron main process +│ └── main.js # Desktop app entry +├── scripts/ # Setup scripts +│ ├── setup-pi.sh # Raspberry Pi setup +│ ├── setup-linux.sh +│ └── setup-windows.ps1 +├── server.js # Express server & API proxy +├── Dockerfile # Container build +├── railway.toml # Railway config +└── package.json +``` -### Architecture +--- -- **Frontend**: React 18 (single-file, no build required) -- **Styling**: CSS-in-JS with CSS variables for theming -- **Maps**: SVG-based rendering (no external tiles) -- **Data**: Currently static, API integration planned +## 🤝 Contributing -### Browser Support +We welcome contributions from the amateur radio community! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. -- Chrome/Chromium 80+ -- Firefox 75+ -- Safari 13+ -- Edge 80+ +### Priority Areas -### Dependencies +1. **Satellite Tracking** - TLE parsing and pass predictions +2. **Contest Calendar** - Integration with contest databases +3. **Rotator Control** - Hamlib integration +4. **Additional APIs** - QRZ, LoTW, ClubLog +5. **Accessibility** - Screen reader support, high contrast modes +6. **Translations** - Internationalization -None! OpenHamClock loads React and Babel from CDN for simplicity. +### How to Contribute -For offline/airgapped deployments, download these files: -- react.production.min.js -- react-dom.production.min.js -- babel.min.js +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request -## Contributing +--- -Contributions are welcome! Areas where help is needed: +## 📜 License -1. **API Integrations** - Connect to live data sources -2. **Satellite Tracking** - SGP4 propagator implementation -3. **Map Improvements** - Better landmass rendering, additional projections -4. **Testing** - Various Pi models and display sizes -5. **Documentation** - User guides, translations -6. **Design** - UI/UX improvements, accessibility +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -## License +--- -MIT License - Free for personal and commercial use. +## 🙏 Acknowledgments -## Acknowledgments +- **Elwood Downey, WB0OEW** - Creator of the original HamClock. Your work inspired thousands of amateur radio operators worldwide. Rest in peace, OM. 🕊️ +- **Leaflet.js** - Outstanding open-source mapping library +- **OpenStreetMap** - Community-driven map data +- **ESRI** - Satellite and specialty map tiles +- **NOAA Space Weather Prediction Center** - Space weather data +- **Parks on the Air (POTA)** - Activator spot API +- **Summits on the Air (SOTA)** - Summit spot API +- **The Amateur Radio Community** - For keeping the spirit of experimentation alive -- **Elwood Downey, WB0OEW** - Creator of the original HamClock. His work inspired thousands of amateur radio operators worldwide. Rest in peace, OM. -- **Amateur Radio Community** - For continued innovation and the spirit of experimentation. +--- -## Contact +## 📞 Contact -- **GitHub**: [https://github.com/accius/openhamclock.git](https://github.com/accius/openhamclock.git) -- **Email**: chris@cjhlighting.com +- **GitHub Issues**: [Report bugs or request features](https://github.com/k0cjh/openhamclock/issues) +- **Discussions**: [Join the conversation](https://github.com/k0cjh/openhamclock/discussions) --- -**73 de K0CJH** +
+ +**73 de K0CJH and the OpenHamClock contributors!** + +*"The original HamClock will cease to function in June 2026. OpenHamClock carries forward Elwood's legacy with modern technology and open-source community development."* -*"The original HamClock will cease to function in June 2026. OpenHamClock aims to carry on Elwood's legacy with a modern, open-source implementation that the community can maintain and improve together."* +
diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..308dc5a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + openhamclock: + build: . + container_name: openhamclock + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - PORT=3000 + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + # Uncomment to set timezone + # environment: + # - TZ=America/Denver + +# For development with hot reload: +# docker compose -f docker-compose.dev.yml up diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 0000000..4266a96 --- /dev/null +++ b/electron/main.js @@ -0,0 +1,287 @@ +/** + * OpenHamClock Electron Main Process + * + * Creates a native desktop application wrapper for OpenHamClock + * Supports Windows, macOS, Linux, and Raspberry Pi + */ + +const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron'); +const path = require('path'); + +// Keep a global reference to prevent garbage collection +let mainWindow; + +// Check if running in development +const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; + +// Start the Express server in production +let server; +if (!isDev) { + // In production, start the built-in server + const express = require('express'); + const serverApp = express(); + const PORT = 3847; // Use a unique port for embedded server + + serverApp.use(express.static(path.join(__dirname, '..', 'public'))); + serverApp.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '..', 'public', 'index.html')); + }); + + server = serverApp.listen(PORT, () => { + console.log(`Embedded server running on port ${PORT}`); + }); +} + +function createWindow() { + // Determine the URL to load + const loadURL = isDev + ? 'http://localhost:3000' + : `http://localhost:3847`; + + // Create the browser window + mainWindow = new BrowserWindow({ + width: 1600, + height: 900, + minWidth: 1024, + minHeight: 600, + title: 'OpenHamClock', + icon: path.join(__dirname, '..', 'public', 'icons', 'icon.png'), + backgroundColor: '#0a0e14', + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + enableRemoteModule: false, + // Preload script for any IPC communication + // preload: path.join(__dirname, 'preload.js') + }, + // Frame options + frame: true, + autoHideMenuBar: false, + }); + + // Load the app + mainWindow.loadURL(loadURL); + + // Open DevTools in development + if (isDev) { + mainWindow.webContents.openDevTools(); + } + + // Handle external links + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: 'deny' }; + }); + + // Cleanup on close + mainWindow.on('closed', () => { + mainWindow = null; + }); + + // Handle fullscreen toggle with F11 + mainWindow.webContents.on('before-input-event', (event, input) => { + if (input.key === 'F11') { + mainWindow.setFullScreen(!mainWindow.isFullScreen()); + event.preventDefault(); + } + }); +} + +// Create application menu +function createMenu() { + const template = [ + { + label: 'File', + submenu: [ + { + label: 'Refresh Data', + accelerator: 'F5', + click: () => { + if (mainWindow) { + mainWindow.webContents.reload(); + } + } + }, + { type: 'separator' }, + { + label: 'Kiosk Mode', + accelerator: 'F11', + click: () => { + if (mainWindow) { + mainWindow.setFullScreen(!mainWindow.isFullScreen()); + } + } + }, + { type: 'separator' }, + { role: 'quit' } + ] + }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forceReload' }, + { type: 'separator' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' } + ] + }, + { + label: 'Map', + submenu: [ + { + label: 'Dark Theme', + accelerator: '1', + click: () => sendMapStyle('dark') + }, + { + label: 'Satellite', + accelerator: '2', + click: () => sendMapStyle('satellite') + }, + { + label: 'Terrain', + accelerator: '3', + click: () => sendMapStyle('terrain') + }, + { + label: 'Streets', + accelerator: '4', + click: () => sendMapStyle('streets') + }, + { + label: 'Topographic', + accelerator: '5', + click: () => sendMapStyle('topo') + }, + { + label: 'Ocean', + accelerator: '6', + click: () => sendMapStyle('ocean') + }, + { + label: 'National Geographic', + accelerator: '7', + click: () => sendMapStyle('natgeo') + }, + { + label: 'Gray', + accelerator: '8', + click: () => sendMapStyle('gray') + } + ] + }, + { + label: 'Help', + submenu: [ + { + label: 'About OpenHamClock', + click: () => { + const { dialog } = require('electron'); + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'About OpenHamClock', + message: 'OpenHamClock v3.0.0', + detail: 'An open-source amateur radio dashboard.\n\nIn memory of Elwood Downey, WB0OEW, creator of the original HamClock.\n\n73 de the OpenHamClock community!' + }); + } + }, + { + label: 'GitHub Repository', + click: () => { + shell.openExternal('https://github.com/k0cjh/openhamclock'); + } + }, + { type: 'separator' }, + { + label: 'Report Issue', + click: () => { + shell.openExternal('https://github.com/k0cjh/openhamclock/issues/new'); + } + }, + { type: 'separator' }, + { + label: 'Toggle Developer Tools', + accelerator: 'F12', + click: () => { + if (mainWindow) { + mainWindow.webContents.toggleDevTools(); + } + } + } + ] + } + ]; + + // macOS specific menu adjustments + if (process.platform === 'darwin') { + template.unshift({ + label: app.getName(), + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }); + } + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +} + +// Send map style change to renderer +function sendMapStyle(style) { + if (mainWindow) { + mainWindow.webContents.executeJavaScript(` + window.postMessage({ type: 'SET_MAP_STYLE', style: '${style}' }, '*'); + `); + } +} + +// App ready +app.whenReady().then(() => { + createWindow(); + createMenu(); + + // macOS: recreate window when dock icon is clicked + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +// Quit when all windows are closed +app.on('window-all-closed', () => { + // On macOS, apps typically stay open until Cmd+Q + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +// Cleanup on quit +app.on('before-quit', () => { + if (server) { + server.close(); + } +}); + +// Security: Prevent navigation to external URLs +app.on('web-contents-created', (event, contents) => { + contents.on('will-navigate', (event, navigationUrl) => { + const parsedUrl = new URL(navigationUrl); + if (parsedUrl.origin !== 'http://localhost:3000' && parsedUrl.origin !== 'http://localhost:3847') { + event.preventDefault(); + shell.openExternal(navigationUrl); + } + }); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..4153f88 --- /dev/null +++ b/package.json @@ -0,0 +1,96 @@ +{ + "name": "openhamclock", + "version": "3.0.0", + "description": "Open-source amateur radio dashboard with real-time space weather, band conditions, DX cluster, and interactive world map", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js", + "electron": "electron electron/main.js", + "electron:build": "electron-builder", + "electron:build:win": "electron-builder --win", + "electron:build:mac": "electron-builder --mac", + "electron:build:linux": "electron-builder --linux", + "electron:build:pi": "electron-builder --linux --armv7l", + "docker:build": "docker build -t openhamclock .", + "docker:run": "docker run -p 3000:3000 openhamclock", + "setup:pi": "bash scripts/setup-pi.sh", + "setup:linux": "bash scripts/setup-linux.sh", + "test": "echo \"No tests yet\" && exit 0" + }, + "keywords": [ + "ham-radio", + "amateur-radio", + "hamclock", + "dx-cluster", + "space-weather", + "pota", + "sota", + "propagation", + "raspberry-pi", + "electron" + ], + "author": "OpenHamClock Contributors", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/k0cjh/openhamclock.git" + }, + "bugs": { + "url": "https://github.com/k0cjh/openhamclock/issues" + }, + "homepage": "https://github.com/k0cjh/openhamclock#readme", + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "node-fetch": "^2.7.0" + }, + "devDependencies": { + "electron": "^28.0.0", + "electron-builder": "^24.9.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "build": { + "appId": "com.openhamclock.app", + "productName": "OpenHamClock", + "directories": { + "output": "dist" + }, + "files": [ + "public/**/*", + "electron/**/*", + "server.js", + "package.json" + ], + "mac": { + "category": "public.app-category.utilities", + "icon": "public/icons/icon.icns", + "target": [ + "dmg", + "zip" + ] + }, + "win": { + "icon": "public/icons/icon.ico", + "target": [ + "nsis", + "portable" + ] + }, + "linux": { + "icon": "public/icons", + "target": [ + "AppImage", + "deb", + "rpm" + ], + "category": "Utility" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true + } + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..6fda73b --- /dev/null +++ b/public/index.html @@ -0,0 +1,929 @@ + + + + + + OpenHamClock - Amateur Radio Dashboard + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/railway.toml b/railway.toml new file mode 100644 index 0000000..34a91be --- /dev/null +++ b/railway.toml @@ -0,0 +1,13 @@ +# Railway Configuration +# https://docs.railway.app/reference/config-as-code + +[build] +builder = "DOCKERFILE" +dockerfilePath = "Dockerfile" + +[deploy] +startCommand = "node server.js" +healthcheckPath = "/api/health" +healthcheckTimeout = 100 +restartPolicyType = "ON_FAILURE" +restartPolicyMaxRetries = 3 diff --git a/scripts/setup-linux.sh b/scripts/setup-linux.sh new file mode 100644 index 0000000..b594d19 --- /dev/null +++ b/scripts/setup-linux.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# +# OpenHamClock - Linux/macOS Setup Script +# +# Quick installation script for Linux and macOS systems +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/k0cjh/openhamclock/main/scripts/setup-linux.sh | bash +# +# Or manually: +# chmod +x setup-linux.sh +# ./setup-linux.sh +# + +set -e + +# Colors +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' + +INSTALL_DIR="$HOME/openhamclock" + +echo -e "${BLUE}" +echo "╔═══════════════════════════════════════════════════════════╗" +echo "║ OpenHamClock Installation Script ║" +echo "╚═══════════════════════════════════════════════════════════╝" +echo -e "${NC}" + +# Check for Node.js +check_node() { + if ! command -v node &> /dev/null; then + echo -e "${YELLOW}Node.js not found. Please install Node.js 18 or later:${NC}" + echo "" + echo " macOS: brew install node" + echo " Ubuntu: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs" + echo " Fedora: sudo dnf install nodejs" + echo " Arch: sudo pacman -S nodejs npm" + echo "" + exit 1 + fi + + NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) + if [ "$NODE_VERSION" -lt 18 ]; then + echo -e "${YELLOW}Node.js version 18 or later required. Current: $(node -v)${NC}" + exit 1 + fi + + echo -e "${GREEN}✓ Node.js $(node -v) detected${NC}" +} + +# Check for Git +check_git() { + if ! command -v git &> /dev/null; then + echo -e "${YELLOW}Git not found. Please install Git first.${NC}" + exit 1 + fi + echo -e "${GREEN}✓ Git detected${NC}" +} + +# Clone or update repository +setup_repo() { + echo -e "${BLUE}>>> Setting up OpenHamClock...${NC}" + + if [ -d "$INSTALL_DIR" ]; then + echo "Updating existing installation..." + cd "$INSTALL_DIR" + git pull + else + echo "Cloning repository..." + git clone https://github.com/k0cjh/openhamclock.git "$INSTALL_DIR" + cd "$INSTALL_DIR" + fi + + # Install dependencies + npm install + + echo -e "${GREEN}✓ Installation complete${NC}" +} + +# Create launcher script +create_launcher() { + cat > "$INSTALL_DIR/run.sh" << EOF +#!/bin/bash +cd "$INSTALL_DIR" +echo "Starting OpenHamClock..." +echo "Open http://localhost:3000 in your browser" +node server.js +EOF + chmod +x "$INSTALL_DIR/run.sh" +} + +# Print instructions +print_instructions() { + echo "" + echo -e "${GREEN}╔═══════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Installation Complete! ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " ${BLUE}To start OpenHamClock:${NC}" + echo "" + echo " cd $INSTALL_DIR && npm start" + echo "" + echo " Or use the launcher: $INSTALL_DIR/run.sh" + echo "" + echo -e " ${BLUE}Then open:${NC} http://localhost:3000" + echo "" + echo -e " ${BLUE}For Electron desktop app:${NC}" + echo " npm run electron" + echo "" + echo -e " ${BLUE}73 de OpenHamClock!${NC}" + echo "" +} + +# Main +main() { + check_node + check_git + setup_repo + create_launcher + print_instructions +} + +main diff --git a/scripts/setup-pi.sh b/scripts/setup-pi.sh new file mode 100644 index 0000000..126fe41 --- /dev/null +++ b/scripts/setup-pi.sh @@ -0,0 +1,355 @@ +#!/bin/bash +# +# OpenHamClock - Raspberry Pi Setup Script +# +# This script configures a Raspberry Pi for kiosk mode operation +# Supports: Pi 3B, 3B+, 4, 5 (32-bit and 64-bit Raspberry Pi OS) +# +# Usage: +# chmod +x setup-pi.sh +# ./setup-pi.sh +# +# Options: +# --kiosk Enable kiosk mode (auto-start on boot) +# --server Install as a server (no GUI) +# --help Show help +# + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +INSTALL_DIR="$HOME/openhamclock" +SERVICE_NAME="openhamclock" +NODE_VERSION="20" + +# Print banner +echo -e "${BLUE}" +echo "╔═══════════════════════════════════════════════════════════╗" +echo "║ ║" +echo "║ ██████╗ ██████╗ ███████╗███╗ ██╗ ║" +echo "║ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ║" +echo "║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ║" +echo "║ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ║" +echo "║ ╚██████╔╝██║ ███████╗██║ ╚████║ ║" +echo "║ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ HAM CLOCK ║" +echo "║ ║" +echo "║ Raspberry Pi Setup Script ║" +echo "║ ║" +echo "╚═══════════════════════════════════════════════════════════╝" +echo -e "${NC}" + +# Parse arguments +KIOSK_MODE=false +SERVER_MODE=false + +while [[ "$#" -gt 0 ]]; do + case $1 in + --kiosk) KIOSK_MODE=true ;; + --server) SERVER_MODE=true ;; + --help) + echo "Usage: ./setup-pi.sh [OPTIONS]" + echo "" + echo "Options:" + echo " --kiosk Enable kiosk mode (fullscreen, auto-start)" + echo " --server Install as headless server only" + echo " --help Show this help message" + exit 0 + ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac + shift +done + +# Check if running on Raspberry Pi +check_raspberry_pi() { + if [ -f /proc/device-tree/model ]; then + MODEL=$(cat /proc/device-tree/model) + echo -e "${GREEN}✓ Detected: $MODEL${NC}" + else + echo -e "${YELLOW}⚠ Warning: This doesn't appear to be a Raspberry Pi${NC}" + read -p "Continue anyway? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + fi +} + +# Update system +update_system() { + echo -e "${BLUE}>>> Updating system packages...${NC}" + sudo apt-get update -qq + sudo apt-get upgrade -y -qq +} + +# Install Node.js +install_nodejs() { + echo -e "${BLUE}>>> Installing Node.js ${NODE_VERSION}...${NC}" + + # Check if Node.js is already installed + if command -v node &> /dev/null; then + CURRENT_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) + if [ "$CURRENT_VERSION" -ge "$NODE_VERSION" ]; then + echo -e "${GREEN}✓ Node.js $(node -v) already installed${NC}" + return + fi + fi + + # Install Node.js via NodeSource + curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | sudo -E bash - + sudo apt-get install -y nodejs + + echo -e "${GREEN}✓ Node.js $(node -v) installed${NC}" +} + +# Install dependencies +install_dependencies() { + echo -e "${BLUE}>>> Installing system dependencies...${NC}" + + PACKAGES="git" + + if [ "$SERVER_MODE" = false ]; then + PACKAGES="$PACKAGES chromium-browser unclutter xdotool x11-xserver-utils" + fi + + sudo apt-get install -y -qq $PACKAGES + echo -e "${GREEN}✓ Dependencies installed${NC}" +} + +# Clone or update repository +setup_repository() { + echo -e "${BLUE}>>> Setting up OpenHamClock...${NC}" + + if [ -d "$INSTALL_DIR" ]; then + echo "Updating existing installation..." + cd "$INSTALL_DIR" + git pull + else + echo "Cloning repository..." + git clone https://github.com/k0cjh/openhamclock.git "$INSTALL_DIR" + cd "$INSTALL_DIR" + fi + + # Install npm dependencies + npm install --production + + echo -e "${GREEN}✓ OpenHamClock installed to $INSTALL_DIR${NC}" +} + +# Create systemd service +create_service() { + echo -e "${BLUE}>>> Creating systemd service...${NC}" + + sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null << EOF +[Unit] +Description=OpenHamClock Server +After=network.target + +[Service] +Type=simple +User=$USER +WorkingDirectory=$INSTALL_DIR +ExecStart=/usr/bin/node server.js +Restart=on-failure +RestartSec=10 +Environment=NODE_ENV=production +Environment=PORT=3000 + +[Install] +WantedBy=multi-user.target +EOF + + sudo systemctl daemon-reload + sudo systemctl enable ${SERVICE_NAME} + sudo systemctl start ${SERVICE_NAME} + + echo -e "${GREEN}✓ Service created and started${NC}" +} + +# Setup kiosk mode +setup_kiosk() { + echo -e "${BLUE}>>> Configuring kiosk mode...${NC}" + + # Disable screen blanking + sudo raspi-config nonint do_blanking 1 2>/dev/null || true + + # Create autostart directory + mkdir -p "$HOME/.config/autostart" + + # Create kiosk launcher script + cat > "$INSTALL_DIR/kiosk.sh" << 'EOF' +#!/bin/bash +# OpenHamClock Kiosk Launcher + +# Wait for desktop +sleep 5 + +# Disable screen saver and power management +xset s off +xset -dpms +xset s noblank + +# Hide mouse cursor +unclutter -idle 1 -root & + +# Wait for server to be ready +while ! curl -s http://localhost:3000/api/health > /dev/null; do + sleep 1 +done + +# Launch Chromium in kiosk mode +chromium-browser \ + --kiosk \ + --noerrdialogs \ + --disable-infobars \ + --disable-session-crashed-bubble \ + --disable-restore-session-state \ + --disable-features=TranslateUI \ + --check-for-update-interval=31536000 \ + --disable-component-update \ + --overscroll-history-navigation=0 \ + --disable-pinch \ + --incognito \ + http://localhost:3000 +EOF + + chmod +x "$INSTALL_DIR/kiosk.sh" + + # Create autostart entry + cat > "$HOME/.config/autostart/openhamclock-kiosk.desktop" << EOF +[Desktop Entry] +Type=Application +Name=OpenHamClock Kiosk +Exec=$INSTALL_DIR/kiosk.sh +Hidden=false +X-GNOME-Autostart-enabled=true +EOF + + # Configure boot for faster startup + if [ -f /boot/config.txt ]; then + # Disable splash screen for faster boot + if ! grep -q "disable_splash=1" /boot/config.txt; then + echo "disable_splash=1" | sudo tee -a /boot/config.txt > /dev/null + fi + + # Allocate more GPU memory + if ! grep -q "gpu_mem=" /boot/config.txt; then + echo "gpu_mem=128" | sudo tee -a /boot/config.txt > /dev/null + fi + fi + + echo -e "${GREEN}✓ Kiosk mode configured${NC}" +} + +# Create helper scripts +create_scripts() { + echo -e "${BLUE}>>> Creating helper scripts...${NC}" + + # Start script + cat > "$INSTALL_DIR/start.sh" << EOF +#!/bin/bash +cd "$INSTALL_DIR" +node server.js +EOF + chmod +x "$INSTALL_DIR/start.sh" + + # Stop script + cat > "$INSTALL_DIR/stop.sh" << EOF +#!/bin/bash +sudo systemctl stop ${SERVICE_NAME} +pkill -f chromium-browser 2>/dev/null || true +pkill -f unclutter 2>/dev/null || true +echo "OpenHamClock stopped" +EOF + chmod +x "$INSTALL_DIR/stop.sh" + + # Restart script + cat > "$INSTALL_DIR/restart.sh" << EOF +#!/bin/bash +sudo systemctl restart ${SERVICE_NAME} +echo "OpenHamClock restarted" +EOF + chmod +x "$INSTALL_DIR/restart.sh" + + # Status script + cat > "$INSTALL_DIR/status.sh" << EOF +#!/bin/bash +echo "=== OpenHamClock Status ===" +sudo systemctl status ${SERVICE_NAME} --no-pager +echo "" +echo "=== Server Health ===" +curl -s http://localhost:3000/api/health | python3 -m json.tool 2>/dev/null || echo "Server not responding" +EOF + chmod +x "$INSTALL_DIR/status.sh" + + echo -e "${GREEN}✓ Helper scripts created${NC}" +} + +# Print summary +print_summary() { + echo "" + echo -e "${GREEN}╔═══════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Installation Complete! ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " ${BLUE}Installation Directory:${NC} $INSTALL_DIR" + 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 "" + echo -e " ${YELLOW}Service Commands:${NC}" + echo " sudo systemctl start ${SERVICE_NAME}" + echo " sudo systemctl stop ${SERVICE_NAME}" + echo " sudo systemctl status ${SERVICE_NAME}" + echo " sudo journalctl -u ${SERVICE_NAME} -f" + echo "" + + if [ "$KIOSK_MODE" = true ]; then + echo -e " ${GREEN}Kiosk Mode:${NC} Enabled" + echo " OpenHamClock will auto-start on boot in fullscreen" + echo " To disable: rm ~/.config/autostart/openhamclock-kiosk.desktop" + echo "" + fi + + echo -e " ${BLUE}73 de OpenHamClock!${NC}" + echo "" + + if [ "$KIOSK_MODE" = true ]; then + read -p "Reboot now to start kiosk mode? (y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + sudo reboot + fi + fi +} + +# Main installation flow +main() { + check_raspberry_pi + update_system + install_nodejs + install_dependencies + setup_repository + create_service + create_scripts + + if [ "$KIOSK_MODE" = true ]; then + setup_kiosk + fi + + print_summary +} + +# Run main +main diff --git a/scripts/setup-windows.ps1 b/scripts/setup-windows.ps1 new file mode 100644 index 0000000..5bd0264 --- /dev/null +++ b/scripts/setup-windows.ps1 @@ -0,0 +1,127 @@ +# OpenHamClock - Windows Setup Script +# +# Run in PowerShell as Administrator: +# Set-ExecutionPolicy Bypass -Scope Process -Force +# .\setup-windows.ps1 +# + +$ErrorActionPreference = "Stop" + +Write-Host "" +Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Blue +Write-Host "║ OpenHamClock Windows Setup ║" -ForegroundColor Blue +Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Blue +Write-Host "" + +$InstallDir = "$env:USERPROFILE\openhamclock" + +# Check for Node.js +function Check-Node { + try { + $nodeVersion = node -v + $versionNumber = [int]($nodeVersion -replace 'v(\d+)\..*', '$1') + + if ($versionNumber -lt 18) { + Write-Host "Node.js version 18 or later required. Current: $nodeVersion" -ForegroundColor Yellow + Write-Host "Download from: https://nodejs.org/" -ForegroundColor Yellow + exit 1 + } + + Write-Host "✓ Node.js $nodeVersion detected" -ForegroundColor Green + } + catch { + Write-Host "Node.js not found. Please install Node.js 18 or later from https://nodejs.org/" -ForegroundColor Yellow + exit 1 + } +} + +# Check for Git +function Check-Git { + try { + git --version | Out-Null + Write-Host "✓ Git detected" -ForegroundColor Green + } + catch { + Write-Host "Git not found. Please install Git from https://git-scm.com/" -ForegroundColor Yellow + exit 1 + } +} + +# Setup repository +function Setup-Repository { + Write-Host ">>> Setting up OpenHamClock..." -ForegroundColor Blue + + if (Test-Path $InstallDir) { + Write-Host "Updating existing installation..." + Set-Location $InstallDir + git pull + } + else { + Write-Host "Cloning repository..." + git clone https://github.com/k0cjh/openhamclock.git $InstallDir + Set-Location $InstallDir + } + + Write-Host "Installing dependencies..." + npm install + + Write-Host "✓ Installation complete" -ForegroundColor Green +} + +# Create desktop shortcut +function Create-Shortcut { + $WshShell = New-Object -ComObject WScript.Shell + $Shortcut = $WshShell.CreateShortcut("$env:USERPROFILE\Desktop\OpenHamClock.lnk") + $Shortcut.TargetPath = "cmd.exe" + $Shortcut.Arguments = "/c cd /d `"$InstallDir`" && npm start" + $Shortcut.WorkingDirectory = $InstallDir + $Shortcut.Description = "OpenHamClock - Amateur Radio Dashboard" + $Shortcut.Save() + + Write-Host "✓ Desktop shortcut created" -ForegroundColor Green +} + +# Create batch file launcher +function Create-Launcher { + $batchContent = @" +@echo off +cd /d "$InstallDir" +echo Starting OpenHamClock... +echo Open http://localhost:3000 in your browser +npm start +pause +"@ + + Set-Content -Path "$InstallDir\start.bat" -Value $batchContent + Write-Host "✓ Launcher created: $InstallDir\start.bat" -ForegroundColor Green +} + +# Print instructions +function Print-Instructions { + Write-Host "" + Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Green + Write-Host "║ Installation Complete! ║" -ForegroundColor Green + Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Green + Write-Host "" + Write-Host " To start OpenHamClock:" -ForegroundColor Blue + Write-Host "" + Write-Host " 1. Double-click the desktop shortcut" + Write-Host " 2. Or run: $InstallDir\start.bat" + Write-Host " 3. Or in PowerShell: cd $InstallDir; npm start" + Write-Host "" + Write-Host " Then open: http://localhost:3000" -ForegroundColor Blue + Write-Host "" + Write-Host " For Electron desktop app:" -ForegroundColor Blue + Write-Host " npm run electron" + Write-Host "" + Write-Host " 73 de OpenHamClock!" -ForegroundColor Blue + Write-Host "" +} + +# Main +Check-Node +Check-Git +Setup-Repository +Create-Launcher +Create-Shortcut +Print-Instructions diff --git a/server.js b/server.js new file mode 100644 index 0000000..0092bc2 --- /dev/null +++ b/server.js @@ -0,0 +1,235 @@ +/** + * OpenHamClock Server + * + * Express server that: + * 1. Serves the static web application + * 2. Proxies API requests to avoid CORS issues + * 3. Provides WebSocket support for future real-time features + * + * Usage: + * node server.js + * PORT=8080 node server.js + */ + +const express = require('express'); +const cors = require('cors'); +const path = require('path'); +const fetch = require('node-fetch'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(cors()); +app.use(express.json()); + +// Serve static files from public directory +app.use(express.static(path.join(__dirname, 'public'))); + +// ============================================ +// API PROXY ENDPOINTS +// ============================================ + +// NOAA Space Weather - Solar Flux +app.get('/api/noaa/flux', async (req, res) => { + try { + const response = await fetch('https://services.swpc.noaa.gov/json/f107_cm_flux.json'); + const data = await response.json(); + res.json(data); + } catch (error) { + console.error('NOAA Flux API error:', error.message); + res.status(500).json({ error: 'Failed to fetch solar flux data' }); + } +}); + +// NOAA Space Weather - K-Index +app.get('/api/noaa/kindex', async (req, res) => { + try { + const response = await fetch('https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json'); + const data = await response.json(); + res.json(data); + } catch (error) { + console.error('NOAA K-Index API error:', error.message); + res.status(500).json({ error: 'Failed to fetch K-index data' }); + } +}); + +// NOAA Space Weather - Sunspots +app.get('/api/noaa/sunspots', async (req, res) => { + try { + const response = await fetch('https://services.swpc.noaa.gov/json/solar-cycle/observed-solar-cycle-indices.json'); + const data = await response.json(); + res.json(data); + } catch (error) { + console.error('NOAA Sunspots API error:', error.message); + res.status(500).json({ error: 'Failed to fetch sunspot data' }); + } +}); + +// NOAA Space Weather - X-Ray Flux +app.get('/api/noaa/xray', async (req, res) => { + try { + const response = await fetch('https://services.swpc.noaa.gov/json/goes/primary/xrays-7-day.json'); + const data = await response.json(); + res.json(data); + } catch (error) { + console.error('NOAA X-Ray API error:', error.message); + res.status(500).json({ error: 'Failed to fetch X-ray data' }); + } +}); + +// POTA Spots +app.get('/api/pota/spots', async (req, res) => { + try { + const response = await fetch('https://api.pota.app/spot/activator'); + const data = await response.json(); + res.json(data); + } catch (error) { + console.error('POTA API error:', error.message); + res.status(500).json({ error: 'Failed to fetch POTA spots' }); + } +}); + +// SOTA Spots +app.get('/api/sota/spots', async (req, res) => { + try { + const response = await fetch('https://api2.sota.org.uk/api/spots/50/all'); + const data = await response.json(); + res.json(data); + } catch (error) { + console.error('SOTA API error:', error.message); + res.status(500).json({ error: 'Failed to fetch SOTA spots' }); + } +}); + +// HamQSL Band Conditions +app.get('/api/hamqsl/conditions', async (req, res) => { + try { + const response = await fetch('https://www.hamqsl.com/solarxml.php'); + const text = await response.text(); + res.set('Content-Type', 'application/xml'); + res.send(text); + } catch (error) { + console.error('HamQSL API error:', error.message); + res.status(500).json({ error: 'Failed to fetch band conditions' }); + } +}); + +// DX Cluster proxy (for future WebSocket implementation) +app.get('/api/dxcluster/spots', async (req, res) => { + try { + // Try DXWatch first + const response = await fetch('https://www.dxwatch.com/api/spots.json?limit=20', { + headers: { 'User-Agent': 'OpenHamClock/3.0' } + }); + if (response.ok) { + const data = await response.json(); + res.json(data); + } else { + // Return empty array if API unavailable + res.json([]); + } + } catch (error) { + console.error('DX Cluster API error:', error.message); + res.json([]); // Return empty array on error + } +}); + +// QRZ Callsign lookup (requires API key) +app.get('/api/qrz/lookup/:callsign', async (req, res) => { + const { callsign } = req.params; + // Note: QRZ requires an API key - this is a placeholder + res.json({ + message: 'QRZ lookup requires API key configuration', + callsign: callsign.toUpperCase() + }); +}); + +// ============================================ +// HEALTH CHECK +// ============================================ + +app.get('/api/health', (req, res) => { + res.json({ + status: 'ok', + version: '3.0.0', + uptime: process.uptime(), + timestamp: new Date().toISOString() + }); +}); + +// ============================================ +// CONFIGURATION ENDPOINT +// ============================================ + +app.get('/api/config', (req, res) => { + res.json({ + version: '3.0.0', + features: { + spaceWeather: true, + pota: true, + sota: true, + dxCluster: true, + satellites: false, // Coming soon + contests: false // Coming soon + }, + refreshIntervals: { + spaceWeather: 300000, + pota: 60000, + sota: 60000, + dxCluster: 30000 + } + }); +}); + +// ============================================ +// CATCH-ALL FOR SPA +// ============================================ + +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +// ============================================ +// START SERVER +// ============================================ + +app.listen(PORT, () => { + console.log(''); + console.log('╔═══════════════════════════════════════════════════════╗'); + console.log('║ ║'); + console.log('║ ██████╗ ██████╗ ███████╗███╗ ██╗ ║'); + console.log('║ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ║'); + console.log('║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ║'); + console.log('║ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ║'); + console.log('║ ╚██████╔╝██║ ███████╗██║ ╚████║ ║'); + console.log('║ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ║'); + console.log('║ ║'); + console.log('║ ██╗ ██╗ █████╗ ███╗ ███╗ ██████╗██╗ ██╗ ██╗ ║'); + console.log('║ ██║ ██║██╔══██╗████╗ ████║██╔════╝██║ ██║ ██╔╝ ║'); + console.log('║ ███████║███████║██╔████╔██║██║ ██║ █████╔╝ ║'); + console.log('║ ██╔══██║██╔══██║██║╚██╔╝██║██║ ██║ ██╔═██╗ ║'); + console.log('║ ██║ ██║██║ ██║██║ ╚═╝ ██║╚██████╗███████╗██║ ██╗ ║'); + console.log('║ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ║'); + console.log('║ ║'); + console.log('╚═══════════════════════════════════════════════════════╝'); + console.log(''); + console.log(` 🌐 Server running at http://localhost:${PORT}`); + console.log(' 📡 API proxy enabled for NOAA, POTA, SOTA, DX Cluster'); + console.log(' 🖥️ Open your browser to start using OpenHamClock'); + console.log(''); + console.log(' In memory of Elwood Downey, WB0OEW'); + console.log(' 73 de OpenHamClock contributors'); + console.log(''); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('SIGTERM received, shutting down gracefully'); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.log('\nShutting down...'); + process.exit(0); +});