Thank you for helping build OpenHamClock! Whether you're fixing a bug, adding a feature, improving docs, or translating — every contribution matters.
New here? Start with docs/ARCHITECTURE.md for a full codebase map.
# 1. Fork and clone
git clone https://github.com/YOUR_USERNAME/openhamclock.git
cd openhamclock
npm ci
git checkout Staging
# 2. Start the backend (Terminal 1)
node server.js
# → Server running on http://localhost:3001
# 3. Start the frontend dev server (Terminal 2)
npm run dev
# → App running on http://localhost:3000 (proxies API to :3001)Open http://localhost:3000 — you should see the full dashboard with live data.
docker compose up
# → App running on http://localhost:3000src/
├── components/ # React UI panels (DXClusterPanel, SolarPanel, LogbookPanel, etc.)
├── hooks/ # Data fetching hooks (useDXCluster, usePOTASpots, etc.)
├── plugins/layers/ # Built-in map layer plugins (satellites, VOACAP, RBN, etc.)
├── plugins/local/ # Your custom layer plugins — auto-discovered, gitignored
├── layouts/ # Page layouts (Modern, Classic, EmComm)
├── DockableApp.jsx # Dockable layout — panel catalog and docking logic
├── contexts/ # React contexts (RigContext)
├── services/ # Client-side stores (logbookStore — IndexedDB logbook)
├── utils/ # Pure utility functions (callsign, geo, filters, awards)
├── pwa/ # Service worker registration (offline mode)
├── lang/ # i18n translation files (one JSON per language)
└── styles/ # CSS files
server.js # Express entry point — mounts routes, SSE, UDP listeners
server/routes/ # API route modules (dxcluster, satellites, propagation, ...)
server/config.js # Env var → runtime config loader
public/ # Static assets, PWA manifest, service worker (sw.js)
rig-bridge/ # Local rig control bridge with its plugin system
dxspider-proxy/ # DX Spider telnet proxy microservice
ohc-cluster/ # OpenHamClock's own DX cluster node
iturhfprop-service/ # ITU-R P.533 propagation microservice
wasm-build/ # P.533 → WebAssembly build for client-side propagation
Full architecture details: docs/ARCHITECTURE.md
- Check existing issues first
- Open a new issue using the Bug Report template
- Include: browser, screen size, console errors, steps to reproduce
- Open an issue using the Feature Request template
- Describe the use case — why is this useful for operators?
- Mockups and screenshots are welcome
See an issue you want to fix? Claim it so others know it's being worked on:
-
Find an issue you'd like to work on
-
Leave a comment containing exactly:
/assign -
The bot will assign the issue to you and react with 👍
No write access required — any GitHub user can self-assign. Once assigned, feel free to ask questions in the issue thread before diving in. If you claimed something and it's no longer on your radar, just leave a comment so someone else can pick it up.
Fixed the bug or confirmed a resolution? Close the issue directly:
-
Leave a comment containing exactly:
/close -
The bot will close the issue and react with 🚀
- Fork the repo and create a branch from
Staging - Make your changes — keep commits focused and descriptive
- Test across themes (dark, light, retro at minimum) and at different screen sizes
- Update the docs — see Documentation below
- Open a PR against
Stagingwith a clear description of what changed and why
⚠️ Important: All pull requests should target theStagingbranch, notmain. TheStagingbranch is always the most up-to-date version of the codebase.Stagingis merged intomainon the monthly release cycle (first Tuesday of the month).
Branch off Staging and use a descriptive prefix:
feature/my-new-panel
fix/pota-frequency-display
docs/update-readme
We use Prettier to enforce consistent formatting across the codebase. This eliminates quote style, indentation, and whitespace noise from PRs so code review can focus on logic.
It happens automatically: After you run npm ci, a git pre-commit hook (via Husky + lint-staged) will auto-format any staged files before each commit. You don't need to think about it.
Manual commands:
# Format everything
npm run format
# Check without writing (what CI runs)
npm run format:checkOur style (.prettierrc): single quotes, semicolons, 2-space indent, 120-char line width, trailing commas.
CI will fail if unformatted code is pushed. If you see a CI failure on the format check, just run npm run format and commit the result.
IDE setup (optional but recommended): Install the Prettier extension for your editor and enable "Format on Save." The .prettierrc and .editorconfig files will be picked up automatically.
Each panel is a self-contained React component in src/components/.
// src/components/MyPanel.jsx
export const MyPanel = ({ data, loading, onSpotClick }) => {
if (loading) return <div>Loading...</div>;
if (!data?.length) return <div>No data</div>;
return (
<div style={{ color: 'var(--text-primary)' }}>
{data.map((item) => (
<div key={item.id} onClick={() => onSpotClick?.(item)}>
{item.callsign} — {item.freq}
</div>
))}
</div>
);
};Each data source has a dedicated hook in src/hooks/.
// src/hooks/useMyData.js
export const useMyData = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch('/api/mydata');
if (res.ok) setData(await res.json());
} catch (err) {
console.error('[MyData]', err);
} finally {
setLoading(false);
}
};
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, []);
return { data, loading };
};All external APIs are proxied through the Express backend with caching. Routes live in modules under server/routes/; each cache needs a TTL and a size cap:
let myCache = { data: null, timestamp: 0 };
const MY_TTL = 5 * 60 * 1000;
app.get('/api/mydata', async (req, res) => {
const now = Date.now();
if (myCache.data && now - myCache.timestamp < MY_TTL) {
return res.json(myCache.data);
}
const data = await fetch('https://api.example.com/data').then((r) => r.json());
myCache = { data, timestamp: now };
res.json(data);
});Pure functions go in src/utils/ — no API calls, no DOM access — and should ship with colocated tests:
// src/utils/myMath.js
export const calculateSomething = (input1, input2) => {
// Pure calculation
return result;
};Create src/plugins/layers/useMyLayer.js (or src/plugins/local/useMyLayer.js for a personal plugin that survives git updates — the local/ directory is gitignored and auto-discovered):
export const metadata = {
id: 'my-layer',
name: 'My Layer',
description: 'What this layer shows',
icon: '🗺️',
category: 'overlay',
defaultEnabled: false,
defaultOpacity: 0.6,
};
export const useLayer = ({ map, enabled, config }) => {
useEffect(() => {
if (!map || !enabled) return;
// Add your Leaflet layers here
return () => {
/* cleanup */
};
}, [map, enabled]);
};Local plugins in src/plugins/local/ need no registration at all — Vite's glob import picks them up. Built-in plugins in src/plugins/layers/ are imported in src/plugins/layerRegistry.js (add one import + one array entry, and optionally a pinned keyboard shortcut). See src/plugins/OpenHamClock-Plugin-Guide.md for the full plugin API.
Custom dockable panels work the same way: drop a .jsx file into src/plugins/local/panels/ exporting metadata ({ id, name, icon }) and a Panel component. Panels receive a stable v1 props contract of { config, t } only, appear under Plugins in the "+" panel picker, and are wrapped in an ErrorBoundary so a broken plugin can't crash the app. See docs/PLUGINS.md for the full guide to both plugin types, including a working example panel.
Five themes: dark, light, legacy, retro, and custom (user-editable). Never hardcode colors — always use CSS variables:
// ✅ Good
<div style={{ color: 'var(--accent-cyan)', background: 'var(--bg-panel)' }}>
// ❌ Bad
<div style={{ color: '#00ddff', background: '#1a1a2e' }}>Key variables: --bg-primary, --bg-secondary, --bg-tertiary, --bg-panel, --border-color, --text-primary, --text-secondary, --text-muted, --accent-amber, --accent-green, --accent-red, --accent-cyan
The UI ships in 16 languages (see src/lang/). To improve one or add a new language:
-
Every language file is a flat JSON of
"dotted.key": "value"pairs. Copysrc/lang/en.jsonfor a new language, or edit the existing file. -
Keys must stay alphabetically sorted — CI enforces this. Fix ordering automatically with:
npm run lang:sort # sorts keys in place npm run lang:check # what CI runs
-
Register a new language in
src/lang/i18n.jsand submit a PR.
Untranslated keys fall back to English, so partial translations are welcome — every string helps.
Unit tests use Vitest and are colocated with the code they cover (foo.js → foo.test.js). See TESTING.md for the full guide.
npm test # watch mode
npm run test:run # single run (what CI does)PRs that touch filtering, parsing, geo math, or other pure utilities should add or update the colocated tests.
Every PR that adds or changes a user-facing feature must update docs/MANUAL.md — the user manual is part of the feature, not an afterthought. If your change affects installation or first-run setup, update docs/QUICKSTART.md too. Reviewers and triage will check for this before merging.
A short paragraph in the right section is enough: what the feature is, where to find it, and any configuration it needs. Match the surrounding tone.
- App loads without console errors
- Works in Dark, Light, and Retro themes
- Responsive at different screen sizes
- If touching server code: memory-safe (caches have TTLs and size caps)
- If adding an API route: includes caching and error handling
- If adding a panel: registered in
DockableApp.jsxpanel definitions (and other layouts where it applies) -
docs/MANUAL.mdupdated for user-facing changes - Existing features still work
# Run tests
npm run test:run
# Check formatting (CI will fail without this)
npm run format:check
# Check translation key ordering (CI will fail without this)
npm run lang:check- The backend handles 2,000+ concurrent connections on the hosted site — be mindful of memory. Every cache needs a TTL and a size cap.
- Use
npm ci, notnpm install, when working from a clean checkout — it installs exactly what's in the committedpackage-lock.json, keeping local dev, CI, and production deterministic. The lockfile stays in git. - Don't commit
.bak,.backup,.old, test scripts, or other debug files. They're in.gitignore. - Frequencies: POTA/SOTA use MHz, some APIs return kHz. Always normalize display to MHz.
- Rig control: The
tuneTo()function inRigContexthandles all unit conversion. Pass the raw spot object.
By contributing, you agree that your contributions will be licensed under the MIT License.
All contributors are listed in the Community tab inside the app (Settings → Community) and linked to their GitHub profiles. When your PR is merged, we'll add you to the contributors wall. Thank you for helping build OpenHamClock — 73!