Skip to content

Development‐Guide

accius edited this page Mar 5, 2026 · 1 revision

Development Guide

Quick Start

git clone https://github.com/YOUR_USERNAME/openhamclock.git
cd openhamclock
npm ci
git checkout Staging

# Terminal 1 — Backend
node server.js

# Terminal 2 — Frontend (hot reload)
npm run dev

All PRs target the Staging branch, not main. We merge Staging → main on a weekly release cycle.

Project Structure

src/
├── components/      # React UI panels (DXClusterPanel, SolarPanel, etc.)
├── hooks/           # Data fetching hooks (useDXCluster, usePOTASpots, etc.)
├── plugins/layers/  # Map layer plugins (satellites, VOACAP, RBN, etc.)
├── layouts/         # Page layouts (Modern, Classic, Dockable)
├── contexts/        # React contexts (RigContext)
├── utils/           # Pure utility functions (callsign, geo, filters)
├── lang/            # i18n translation files (15 languages)
└── styles/          # CSS files

server.js            # Express backend — all API routes, SSE, MQTT, data proxying
rig-listener/        # Standalone USB rig control agent
rig-bridge/          # Plugin-based rig control with web UI
wsjtx-relay/         # WSJT-X UDP → HTTPS relay agent

Full architecture: docs/ARCHITECTURE.md

Code Formatting

Prettier enforces consistent formatting. A pre-commit hook auto-formats staged files.

npm run format         # Format everything
npm run format:check   # Check without writing (what CI runs)

Style: single quotes, semicolons, 2-space indent, 120-char line width, trailing commas.

Adding a New Panel

Each panel is a self-contained React component in src/components/:

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>
  );
};

Wire it into all three layouts (Modern, Classic, Dockable).

Adding a Data Hook

Each data source has a hook in src/hooks/:

export const useMyData = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      const res = await fetch('/api/mydata');
      if (res.ok) setData(await res.json());
      setLoading(false);
    };
    fetchData();
    const interval = setInterval(fetchData, 30000);
    return () => clearInterval(interval);
  }, []);

  return { data, loading };
};

Adding a Map Layer Plugin

Create src/plugins/layers/useMyLayer.js:

export const metadata = {
  id: 'my_layer',
  name: 'My Layer',
  description: 'What this layer shows',
  category: 'amateur',
  defaultEnabled: false,
  defaultOpacity: 0.7,
  version: '1.0.0',
};

export function useLayer({ map, enabled, opacity }) {
  useEffect(() => {
    if (!map || !enabled) return;
    // Add Leaflet layers here
    return () => { /* cleanup */ };
  }, [map, enabled]);
}

The layer registry auto-discovers plugins — no manual registration needed.

Adding an API Route

All external APIs are proxied through server.js with caching:

let myCache = { data: null, timestamp: 0 };
const MY_TTL = 5 * 60 * 1000;

app.get('/api/mydata', async (req, res) => {
  if (myCache.data && Date.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: Date.now() };
  res.json(data);
});

Important: Every cache needs a TTL and a size cap. The server handles 2,000+ concurrent connections.

Theming

Three themes: dark, light, retro. Never hardcode colors — 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

Translations

Translation files are in src/lang/. When adding new user-facing strings:

  1. Add the key to en.json with the English text
  2. For non-English files, either translate properly or omit the key (i18next falls back to English automatically)
  3. Do not put English text in non-English files — it masks missing translations

Branch Naming

feature/my-new-panel
fix/pota-frequency-display
docs/update-readme

Testing Checklist

  • App loads without console errors
  • Works in Dark, Light, and Retro themes
  • Responsive at different screen sizes
  • If touching server.js: caches have TTLs and size caps
  • If adding an API route: includes caching and error handling
  • If adding a panel: wired into all three layouts
  • Existing features still work

Claiming Issues

Comment /assign on any GitHub issue to self-assign it. Comment /close to close a resolved issue.

Clone this wiki locally