From e95faf6d827226ddfc8478a2d27dd58603511578 Mon Sep 17 00:00:00 2001 From: accius Date: Tue, 19 May 2026 22:09:51 -0400 Subject: [PATCH 1/7] fix(dxspider-proxy): stop quiet-band connection churn + fix auth detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The socket idle timeout (60s) was the shortest of all failure timers, so any 60s gap between spots — normal on quiet bands overnight — tore down a healthy connection. Keepalive (120s) was too slow to prevent it and the 180s activity watchdog (the graceful node-failover) never got to run. - socket timeout 60s -> 300s, as a last-resort TCP backstop; the 180s activity watchdog now owns failover as designed - keepalive 120s -> 60s so the connection stays warm - destroy the socket in the 'timeout' handler — Node does not auto-close on timeout, leaving a zombie socket that kept feeding data and spuriously reset the failover counter after [RECONNECT] - mark authenticated on first spot; the DXSpider prompt has no trailing newline so prompt-based detection never matched, and sh/dx output lines ("...de Helmut") false-matched the prompt regex Co-Authored-By: Claude Opus 4.7 (1M context) --- dxspider-proxy/server.js | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/dxspider-proxy/server.js b/dxspider-proxy/server.js index f09894c6..18d18de6 100644 --- a/dxspider-proxy/server.js +++ b/dxspider-proxy/server.js @@ -30,9 +30,13 @@ const CONFIG = { reconnectDelayMs: 10000, // 10 seconds between reconnect attempts maxReconnectAttempts: 3, cleanupIntervalMs: 60000, // 1 minute - keepAliveIntervalMs: 120000, // 2 minutes - send keepalive + keepAliveIntervalMs: 60000, // 1 minute - send keepalive (must stay < socketTimeoutMs) activityTimeoutMs: 180000, // 3 minutes - if no spots, assume dead and failover authTimeoutMs: 30000, // 30 seconds - if no prompt after login, try next node + // Last-resort TCP backstop. Must be LONGER than activityTimeoutMs so the + // graceful node-failover watchdog acts first. A 60s value here used to + // preempt it and tear down healthy connections during quiet-band gaps. + socketTimeoutMs: 300000, // 5 minutes }; // State @@ -242,7 +246,7 @@ const connect = () => { log('CONNECT', `Attempting connection to ${node.name} (${node.host}:${node.port})`); client = new net.Socket(); - client.setTimeout(60000); // 60 second timeout + client.setTimeout(CONFIG.socketTimeoutMs); client.connect(node.port, node.host, () => { connected = true; @@ -313,6 +317,13 @@ const connect = () => { if (spot) { addSpot(spot); resetActivityWatchdog(); // Got a spot, connection is healthy + // A flowing spot is definitive proof login succeeded. The DXSpider + // prompt has no trailing newline so it never arrives as a complete + // line — prompt-based detection alone is unreliable. + if (!authenticated) { + authenticated = true; + log('AUTH', 'Login confirmed (spot stream active)'); + } // Only reset failover counter if connection has been stable for 60s+ // A few spots before a timeout isn't truly healthy — it traps us // on a flaky node that connects briefly then drops @@ -328,8 +339,10 @@ const connect = () => { continue; } - // Detect auth completion - DX Spider sends "callsign de NODE >" prompt - if (!authenticated && /\sde\s+\S+\s*>/.test(trimmed)) { + // Detect auth completion - DX Spider sends "callsign de NODE >" prompt. + // Exclude lines containing '<' — sh/dx output (e.g. "...de Helmut") + // otherwise false-matches this pattern. + if (!authenticated && !trimmed.includes('<') && /\sde\s+\S+\s*>/.test(trimmed)) { authenticated = true; log('AUTH', `Login confirmed: ${trimmed.substring(0, 80)}`); resetActivityWatchdog(); // Auth done, start watching for spots @@ -344,6 +357,16 @@ const connect = () => { client.on('timeout', () => { log('TIMEOUT', 'Connection timed out'); connecting = false; + // Node does NOT auto-close a socket on timeout. Without this teardown the + // old socket keeps emitting 'data' until the next connect(), spuriously + // logging "Connection stable" and resetting the failover counter. + if (client) { + try { + client.removeAllListeners(); + client.destroy(); + } catch (e) {} + client = null; + } handleDisconnect(); }); From 96f4bfe2425f914cdce4c67d588a843710939eb5 Mon Sep 17 00:00:00 2001 From: accius Date: Wed, 20 May 2026 06:46:26 -0400 Subject: [PATCH 2/7] Revert "fix(dxspider-proxy): stop quiet-band connection churn + fix auth detection" This reverts commit e95faf6. The proxy fix was cherry-picked to main prematurely. It will be validated on Staging first and reach main on the normal release schedule. The fix remains intact on Staging (e7826d9). Co-Authored-By: Claude Opus 4.7 (1M context) --- dxspider-proxy/server.js | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/dxspider-proxy/server.js b/dxspider-proxy/server.js index 18d18de6..f09894c6 100644 --- a/dxspider-proxy/server.js +++ b/dxspider-proxy/server.js @@ -30,13 +30,9 @@ const CONFIG = { reconnectDelayMs: 10000, // 10 seconds between reconnect attempts maxReconnectAttempts: 3, cleanupIntervalMs: 60000, // 1 minute - keepAliveIntervalMs: 60000, // 1 minute - send keepalive (must stay < socketTimeoutMs) + keepAliveIntervalMs: 120000, // 2 minutes - send keepalive activityTimeoutMs: 180000, // 3 minutes - if no spots, assume dead and failover authTimeoutMs: 30000, // 30 seconds - if no prompt after login, try next node - // Last-resort TCP backstop. Must be LONGER than activityTimeoutMs so the - // graceful node-failover watchdog acts first. A 60s value here used to - // preempt it and tear down healthy connections during quiet-band gaps. - socketTimeoutMs: 300000, // 5 minutes }; // State @@ -246,7 +242,7 @@ const connect = () => { log('CONNECT', `Attempting connection to ${node.name} (${node.host}:${node.port})`); client = new net.Socket(); - client.setTimeout(CONFIG.socketTimeoutMs); + client.setTimeout(60000); // 60 second timeout client.connect(node.port, node.host, () => { connected = true; @@ -317,13 +313,6 @@ const connect = () => { if (spot) { addSpot(spot); resetActivityWatchdog(); // Got a spot, connection is healthy - // A flowing spot is definitive proof login succeeded. The DXSpider - // prompt has no trailing newline so it never arrives as a complete - // line — prompt-based detection alone is unreliable. - if (!authenticated) { - authenticated = true; - log('AUTH', 'Login confirmed (spot stream active)'); - } // Only reset failover counter if connection has been stable for 60s+ // A few spots before a timeout isn't truly healthy — it traps us // on a flaky node that connects briefly then drops @@ -339,10 +328,8 @@ const connect = () => { continue; } - // Detect auth completion - DX Spider sends "callsign de NODE >" prompt. - // Exclude lines containing '<' — sh/dx output (e.g. "...de Helmut") - // otherwise false-matches this pattern. - if (!authenticated && !trimmed.includes('<') && /\sde\s+\S+\s*>/.test(trimmed)) { + // Detect auth completion - DX Spider sends "callsign de NODE >" prompt + if (!authenticated && /\sde\s+\S+\s*>/.test(trimmed)) { authenticated = true; log('AUTH', `Login confirmed: ${trimmed.substring(0, 80)}`); resetActivityWatchdog(); // Auth done, start watching for spots @@ -357,16 +344,6 @@ const connect = () => { client.on('timeout', () => { log('TIMEOUT', 'Connection timed out'); connecting = false; - // Node does NOT auto-close a socket on timeout. Without this teardown the - // old socket keeps emitting 'data' until the next connect(), spuriously - // logging "Connection stable" and resetting the failover counter. - if (client) { - try { - client.removeAllListeners(); - client.destroy(); - } catch (e) {} - client = null; - } handleDisconnect(); }); From 7240a2e71712dc3e9dc4831ce326b4be7b380607 Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Wed, 27 May 2026 20:53:28 +0100 Subject: [PATCH 3/7] tweak and add new container image workflows --- .../workflows/docker-dxspider-proxy-image.yml | 89 +++++++++++++++++++ .github/workflows/docker-iturhfprop-image.yml | 89 +++++++++++++++++++ ...{docker-image.yml => docker-ohc-image.yml} | 28 ++++-- 3 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/docker-dxspider-proxy-image.yml create mode 100644 .github/workflows/docker-iturhfprop-image.yml rename .github/workflows/{docker-image.yml => docker-ohc-image.yml} (88%) diff --git a/.github/workflows/docker-dxspider-proxy-image.yml b/.github/workflows/docker-dxspider-proxy-image.yml new file mode 100644 index 00000000..ca736e26 --- /dev/null +++ b/.github/workflows/docker-dxspider-proxy-image.yml @@ -0,0 +1,89 @@ +name: Build and Publish DXSpider Proxy Docker Image + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/dxspider-proxy + +on: + push: + branches: ['*'] + paths: + - 'dxspider-proxy/**' + - '.github/workflows/docker-dxspider-proxy-image.yml' + pull_request: + branches: + - Staging + - main + paths: + - 'dxspider-proxy/**' + - '.github/workflows/docker-dxspider-proxy-image.yml' + release: + types: [published] + # Allows running this workflow manually from the Actions tab + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix={{branch}}-,enable=${{ github.event_name != 'release' }} + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha,scope=${{ github.workflow }} + cache-to: type=gha,scope=${{ github.workflow }},mode=max + provenance: true + sbom: true + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/docker-iturhfprop-image.yml b/.github/workflows/docker-iturhfprop-image.yml new file mode 100644 index 00000000..9d531463 --- /dev/null +++ b/.github/workflows/docker-iturhfprop-image.yml @@ -0,0 +1,89 @@ +name: Build and Publish iturhfprop Docker Image + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/iturhfprop + +on: + push: + branches: ['*'] + paths: + - 'iturhfprop-service/**' + - '.github/workflows/docker-iturhfprop-image.yml' + pull_request: + branches: + - Staging + - main + paths: + - 'iturhfprop-service/**' + - '.github/workflows/docker-iturhfprop-image.yml' + release: + types: [published] + # Allows running this workflow manually from the Actions tab + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix={{branch}}-,enable=${{ github.event_name != 'release' }} + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha,scope=${{ github.workflow }} + cache-to: type=gha,scope=${{ github.workflow }},mode=max + provenance: true + sbom: true + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-ohc-image.yml similarity index 88% rename from .github/workflows/docker-image.yml rename to .github/workflows/docker-ohc-image.yml index 1a78e483..66fc3891 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-ohc-image.yml @@ -1,22 +1,33 @@ -name: Build and Publish Docker Image +name: Build and Publish OHC Docker Image + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} on: push: - branches: ['main'] + branches: ['*'] + paths-ignore: + - 'iturhfprop-service/**' + - 'dxspider-proxy/**' + pull_request: + branches: + - Staging + - main + paths-ignore: + - 'iturhfprop-service/**' + - 'dxspider-proxy/**' release: types: [published] + # Allows running this workflow manually from the Actions tab + workflow_dispatch: permissions: contents: read packages: write -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - jobs: build-and-push: - if: github.repository == 'accius/openhamclock' runs-on: ubuntu-latest permissions: contents: write @@ -60,7 +71,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} @@ -71,7 +82,6 @@ jobs: sbom: true - name: Generate artifact attestation - if: github.event_name != 'pull_request' uses: actions/attest-build-provenance@v1 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} From aef1d2ba3ba1502a4df470b935e44f1267fadd9a Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Wed, 27 May 2026 21:58:05 +0100 Subject: [PATCH 4/7] update action versions due to Node 20 deprecation --- .../workflows/docker-dxspider-proxy-image.yml | 14 +++++++------- .github/workflows/docker-iturhfprop-image.yml | 16 ++++++++-------- .github/workflows/docker-ohc-image.yml | 14 +++++++------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/docker-dxspider-proxy-image.yml b/.github/workflows/docker-dxspider-proxy-image.yml index ca736e26..a782345b 100644 --- a/.github/workflows/docker-dxspider-proxy-image.yml +++ b/.github/workflows/docker-dxspider-proxy-image.yml @@ -37,16 +37,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to the Container registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -54,7 +54,7 @@ jobs: - name: Extract metadata (tags, labels) id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -68,7 +68,7 @@ jobs: - name: Build and push Docker image id: push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . push: true @@ -82,7 +82,7 @@ jobs: sbom: true - name: Generate artifact attestation - uses: actions/attest-build-provenance@v1 + uses: actions/attest-build-provenance@v4 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} diff --git a/.github/workflows/docker-iturhfprop-image.yml b/.github/workflows/docker-iturhfprop-image.yml index 9d531463..42f268f1 100644 --- a/.github/workflows/docker-iturhfprop-image.yml +++ b/.github/workflows/docker-iturhfprop-image.yml @@ -1,4 +1,4 @@ -name: Build and Publish iturhfprop Docker Image +name: Build and Publish ITU-R-HFprop Docker Image env: REGISTRY: ghcr.io @@ -37,16 +37,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to the Container registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -54,7 +54,7 @@ jobs: - name: Extract metadata (tags, labels) id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -68,7 +68,7 @@ jobs: - name: Build and push Docker image id: push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . push: true @@ -82,7 +82,7 @@ jobs: sbom: true - name: Generate artifact attestation - uses: actions/attest-build-provenance@v1 + uses: actions/attest-build-provenance@v4 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} diff --git a/.github/workflows/docker-ohc-image.yml b/.github/workflows/docker-ohc-image.yml index 66fc3891..04e9c567 100644 --- a/.github/workflows/docker-ohc-image.yml +++ b/.github/workflows/docker-ohc-image.yml @@ -37,16 +37,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to the Container registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -54,7 +54,7 @@ jobs: - name: Extract metadata (tags, labels) id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -68,7 +68,7 @@ jobs: - name: Build and push Docker image id: push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . push: true @@ -82,7 +82,7 @@ jobs: sbom: true - name: Generate artifact attestation - uses: actions/attest-build-provenance@v1 + uses: actions/attest-build-provenance@v4 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} From b74b322542979853f791f801119e674e18ce70f6 Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Thu, 28 May 2026 15:37:43 +0100 Subject: [PATCH 5/7] update docker documentation for all components --- ...xy-image.yml => docker-dxspider-proxy.yml} | 6 +-- ...mage.yml => docker-iturhfprop-service.yml} | 8 +-- ...-ohc-image.yml => docker-openhamclock.yml} | 2 +- README.md | 54 ++++++++----------- docker-compose.yml | 1 + docs/DOCKER.md | 54 ++++++++++++++++++- dxspider-proxy/README.md | 30 ++++++++++- iturhfprop-service/README.md | 30 ++++++++++- 8 files changed, 139 insertions(+), 46 deletions(-) rename .github/workflows/{docker-dxspider-proxy-image.yml => docker-dxspider-proxy.yml} (93%) rename .github/workflows/{docker-iturhfprop-image.yml => docker-iturhfprop-service.yml} (90%) rename .github/workflows/{docker-ohc-image.yml => docker-openhamclock.yml} (98%) diff --git a/.github/workflows/docker-dxspider-proxy-image.yml b/.github/workflows/docker-dxspider-proxy.yml similarity index 93% rename from .github/workflows/docker-dxspider-proxy-image.yml rename to .github/workflows/docker-dxspider-proxy.yml index a782345b..1e5858ab 100644 --- a/.github/workflows/docker-dxspider-proxy-image.yml +++ b/.github/workflows/docker-dxspider-proxy.yml @@ -1,4 +1,4 @@ -name: Build and Publish DXSpider Proxy Docker Image +name: Build and Publish dxspider-proxy container image env: REGISTRY: ghcr.io @@ -9,14 +9,14 @@ on: branches: ['*'] paths: - 'dxspider-proxy/**' - - '.github/workflows/docker-dxspider-proxy-image.yml' + - '.github/workflows/docker-dxspider-proxy.yml' pull_request: branches: - Staging - main paths: - 'dxspider-proxy/**' - - '.github/workflows/docker-dxspider-proxy-image.yml' + - '.github/workflows/docker-dxspider-proxy.yml' release: types: [published] # Allows running this workflow manually from the Actions tab diff --git a/.github/workflows/docker-iturhfprop-image.yml b/.github/workflows/docker-iturhfprop-service.yml similarity index 90% rename from .github/workflows/docker-iturhfprop-image.yml rename to .github/workflows/docker-iturhfprop-service.yml index 42f268f1..db0922e0 100644 --- a/.github/workflows/docker-iturhfprop-image.yml +++ b/.github/workflows/docker-iturhfprop-service.yml @@ -1,22 +1,22 @@ -name: Build and Publish ITU-R-HFprop Docker Image +name: Build and Publish iturhfprop-service container image env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository_owner }}/iturhfprop + IMAGE_NAME: ${{ github.repository_owner }}/iturhfprop-service on: push: branches: ['*'] paths: - 'iturhfprop-service/**' - - '.github/workflows/docker-iturhfprop-image.yml' + - '.github/workflows/docker-iturhfprop-service.yml' pull_request: branches: - Staging - main paths: - 'iturhfprop-service/**' - - '.github/workflows/docker-iturhfprop-image.yml' + - '.github/workflows/docker-iturhfprop-service.yml' release: types: [published] # Allows running this workflow manually from the Actions tab diff --git a/.github/workflows/docker-ohc-image.yml b/.github/workflows/docker-openhamclock.yml similarity index 98% rename from .github/workflows/docker-ohc-image.yml rename to .github/workflows/docker-openhamclock.yml index 04e9c567..95e75a03 100644 --- a/.github/workflows/docker-ohc-image.yml +++ b/.github/workflows/docker-openhamclock.yml @@ -1,4 +1,4 @@ -name: Build and Publish OHC Docker Image +name: Build and Publish OHC container image env: REGISTRY: ghcr.io diff --git a/README.md b/README.md index d9178da0..0ac6923d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,14 @@ OpenHamClock brings DX cluster spots, space weather, propagation predictions, PO ## Quick Start -### Prerequisites +### Container Deployment + +Container images are available from the github container registry at `ghcr.io/accius/openhamclock` +Follow [the quick-start steps](docs/DOCKER.md#quick-start-zero-config) + +### Local Install + +#### Prerequisites - **Node.js v20.19 or later** (v22.12+ also supported) — required by Vite and the Express backend. The version of Node.js shipped by default in most Linux distributions @@ -54,7 +61,7 @@ OpenHamClock brings DX cluster spots, space weather, propagation predictions, PO > ⚠️ **Ubuntu / Debian users:** Do **not** use `apt install nodejs` — the packaged version > is v18 which is below the minimum required. Use NodeSource or nvm as shown above. -### Install & run +#### Install & run ```bash git clone https://github.com/accius/openhamclock.git @@ -87,6 +94,16 @@ node server.js npm run dev ``` +### Enabling Client-side Propagation Calculation (without having to build it) + +If you want your clients (using your local server) to use the p533 modules for client side propagation calculation rather than the very rough estimates (which are based on band and time of day), run the following as the user who can write to your repository. + +```bash +scripts/fetch-wasm.sh +``` + +from the top-level distribution directory, and restart the server. + --- ## Table of Contents @@ -446,7 +463,7 @@ HF propagation reliability predictions between your station (DE) and whatever DX **Standard mode:** Uses a built-in propagation model based on current SFI, SSN, Kp, great-circle path distance, solar zenith angle, geomagnetic latitude, and estimated MUF (Maximum Usable Frequency) for each band. -**ITU-R P.533-14 predictions:** By default, all installs use the public OpenHamClock ITURHFProp prediction service for ITU-R P.533-14 propagation calculations — the international standard for HF propagation prediction. If you prefer to self-host, deploy the optional ITURHFProp microservice (in the `iturhfprop-service/` directory) and set `ITURHFPROP_URL` in `.env` to your own instance. +**ITU-R P.533-14 predictions:** By default, all installs use the public OpenHamClock ITURHFProp prediction service for ITU-R P.533-14 propagation calculations — the international standard for HF propagation prediction. If you prefer to self-host, deploy the optional ITURHFProp microservice (documentation and deployment examples in `iturhfprop-service/README.md`) **Hybrid correction:** When ionosonde data is available from `prop.kc2g.com`, the system applies real-time corrections based on actual measured ionospheric conditions rather than just modeled values. This can catch unusual propagation events that models miss. @@ -1042,31 +1059,7 @@ The Pi setup script installs Node.js 22 LTS, clones the repository, builds the f ### Using Docker -**Docker Compose (recommended):** - -```bash -docker-compose up -d -``` - -**Manual Docker build:** - -```bash -docker build -t openhamclock . -docker run -d -p 3000:3000 -p 2237:2237/udp --name openhamclock openhamclock -``` - -The Dockerfile uses a multi-stage build: Stage 1 compiles the React frontend with Vite, Stage 2 creates a minimal production image with only the server and built assets. The UDP port mapping (`-p 2237:2237/udp`) is only needed if you use WSJT-X integration. - -**Environment variables:** Pass your configuration via Docker environment variables: - -```bash -docker run -d \ - -p 3000:3000 \ - -e CALLSIGN=K0CJH \ - -e LOCATOR=EN10 \ - -e HOST=0.0.0.0 \ - openhamclock -``` +Follow the [docker documentation](docs/DOCKER.md) ### Railway (Cloud) @@ -1202,10 +1195,7 @@ On local installs, you can also click the **UPDATE** button in the header to sta ### Docker -```bash -docker-compose pull -docker-compose up -d -``` +[Docker documentation on updating](docs/DOCKER.md#updating) ### Railway diff --git a/docker-compose.yml b/docker-compose.yml index 0933d9a4..0226daa8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: - path: .env required: false build: . + image: ghcr.io/accius/openhamclock:latest container_name: openhamclock ports: - '3000:3000' # HTTP diff --git a/docs/DOCKER.md b/docs/DOCKER.md index 534909b1..d7c9fadb 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -2,12 +2,22 @@ ## Quick Start (Zero Config) +Docker compose is the recommended way to deploy: + ```bash git clone https://github.com/OpenHamClock/openhamclock.git cd openhamclock docker compose up -d ``` +or you can also use traditional docker commands: + +```bash +docker run -d -p 3000:3000 --name openhamclock ghcr.io/accius/openhamclock:latest +``` + +This will pull the latest container image and start the OpenHamClock container + Open **** — that's it. OpenHamClock runs with sensible defaults. ## Customize Your Station @@ -71,9 +81,11 @@ If running behind nginx/Caddy/Traefik, you may want to override the health check HEALTH_ENDPOINT=http://localhost:3000/api/health ``` -## Volumes (Optional) +## Data persistence + +To persist stats and settings across container rebuilds you can either use volumes or bind mounts -To persist stats and settings across container rebuilds: +[Volumes](https://docs.docker.com/engine/storage/volumes/): ```yaml services: @@ -84,12 +96,43 @@ volumes: ohc-data: ``` +[Bind mounts](https://docs.docker.com/engine/storage/bind-mounts/): + +```yaml +services: + openhamclock: + volumes: + - ./ohc-data:/data +``` + ## Updating +Pull the latest image: + +```bash +# If using compose +docker compose pull + +# If using plain docker cli +docker pull ghcr.io/accius/openhamclock:latest +``` + +or build from the main branch: + ```bash git pull docker compose build --no-cache +``` + +then restart container with latest image: + +```bash +# If using compose docker compose up -d + +# If using plain docker cli +docker rm -f openhamclock +docker run -d -p 3000:3000 --name openhamclock ghcr.io/accius/openhamclock:latest ``` ## Configuration Priority @@ -114,3 +157,10 @@ See `.env.example` for the complete list with descriptions. Key sections: - **N1MM** — `N1MM_UDP_ENABLED`, `N1MM_UDP_PORT` - **Weather** — `OPENWEATHER_API_KEY`, `VITE_AMBIENT_*` - **Advanced** — `ITURHFPROP_URL`, `HEALTH_ENDPOINT`, `CORS_ORIGINS` + +## Other Microservices + +You can also self-host other microservices, check their respective documentation for details: + +- [iturhfprop-service](../iturhfprop-service/README.md#docker) +- [dxspider-proxy](../dxspider-proxy/README.md#docker) diff --git a/dxspider-proxy/README.md b/dxspider-proxy/README.md index cc5cf79e..359ffd6a 100644 --- a/dxspider-proxy/README.md +++ b/dxspider-proxy/README.md @@ -164,9 +164,33 @@ The service will automatically start and connect to DX Spider. ### Docker +#### Compose + +When using a compose file for OpenHamClock, you can add it as an additional service: + +```yaml +services: + openhamclock: ... + + dxspider-proxy: + build: + context: ./dxspider-proxy # path to this subdirectory in the openhamclock repo + image: ghcr.io/accius/dxspider-proxy:latest + restart: unless-stopped + container_name: dxspider-proxy +``` + +#### CLI + ```bash +# Build docker build -t dxspider-proxy . -docker run -p 3001:3001 -e CALLSIGN=YOURCALL dxspider-proxy + +# or Pull +docker pull ghcr.io/accius/dxspider-proxy:latest + +# Run +docker run -p 3001:3001 -e CALLSIGN=YOURCALL ghcr.io/accius/dxspider-proxy:latest ``` ### Local Development @@ -178,7 +202,9 @@ CALLSIGN=YOURCALL npm start ## Using with OpenHamClock -Once deployed, update your OpenHamClock configuration to use this proxy as a DX cluster source: +If using docker, edit the `DXSPIDER_PROXY_URL` variable in your `.env` file + +else, update your OpenHamClock configuration to use this proxy as a DX cluster source: ```text https://your-proxy-url.railway.app/api/dxcluster/spots diff --git a/iturhfprop-service/README.md b/iturhfprop-service/README.md index f44e4d36..a150cfc0 100644 --- a/iturhfprop-service/README.md +++ b/iturhfprop-service/README.md @@ -111,17 +111,41 @@ The Dockerfile will: ### Docker +#### Compose + +If using a compose file, you can also add it as an additional service: + +```yaml +services: + openhamclock: ... + + iturhfprop: + build: + context: ./iturhfprop-service # path to this subdirectory in the openhamclock repo + image: ghcr.io/accius/iturhfprop-service:latest + restart: unless-stopped + container_name: iturhfprop +``` + +#### CLI + ```bash # Build docker build -t iturhfprop-service . +# or Pull +docker pull ghcr.io/accius/iturhfprop-service:latest + # Run -docker run -p 3000:3000 iturhfprop-service +docker run -p 3000:3000 ghcr.io/accius/iturhfprop-service:latest # Test curl http://localhost:3000/api/health ``` +And then enable it in your `.env` file as: +`ITURHFPROP_URL=http://iturhfprop:3000` + ### Local Development Download ITURHFProp binaries from the [official release](https://github.com/ITU-R-Study-Group-3/ITU-R-HF/releases): @@ -151,7 +175,9 @@ npm start ## Integration with OpenHamClock -In your OpenHamClock server.js, add: +If using docker, edit the `ITURHFPROP_URL` variable in your `.env` file. + +If running locally, in your OpenHamClock server.js, add: ```javascript const ITURHFPROP_SERVICE = process.env.ITURHFPROP_URL || 'http://localhost:3001'; From 2a8dd3f08df9146efd99fdfda3c522b2691b8233 Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Thu, 28 May 2026 17:07:38 +0100 Subject: [PATCH 6/7] fix sha metadata config for docker image tagging --- .github/workflows/docker-dxspider-proxy.yml | 2 +- .github/workflows/docker-iturhfprop-service.yml | 2 +- .github/workflows/docker-openhamclock.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-dxspider-proxy.yml b/.github/workflows/docker-dxspider-proxy.yml index 1e5858ab..ae7b2a35 100644 --- a/.github/workflows/docker-dxspider-proxy.yml +++ b/.github/workflows/docker-dxspider-proxy.yml @@ -60,7 +60,7 @@ jobs: tags: | type=ref,event=branch type=ref,event=pr - type=sha,prefix={{branch}}-,enable=${{ github.event_name != 'release' }} + type=sha,format=long type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} diff --git a/.github/workflows/docker-iturhfprop-service.yml b/.github/workflows/docker-iturhfprop-service.yml index db0922e0..162e6072 100644 --- a/.github/workflows/docker-iturhfprop-service.yml +++ b/.github/workflows/docker-iturhfprop-service.yml @@ -60,7 +60,7 @@ jobs: tags: | type=ref,event=branch type=ref,event=pr - type=sha,prefix={{branch}}-,enable=${{ github.event_name != 'release' }} + type=sha,format=long type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} diff --git a/.github/workflows/docker-openhamclock.yml b/.github/workflows/docker-openhamclock.yml index 95e75a03..07d53895 100644 --- a/.github/workflows/docker-openhamclock.yml +++ b/.github/workflows/docker-openhamclock.yml @@ -60,7 +60,7 @@ jobs: tags: | type=ref,event=branch type=ref,event=pr - type=sha,prefix={{branch}}-,enable=${{ github.event_name != 'release' }} + type=sha,format=long type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} From acac2127546be35708a9a50e4f629a9248101d8c Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Thu, 28 May 2026 20:49:17 +0100 Subject: [PATCH 7/7] add conditional push to allow fork building when using GITHUB_TOKEN --- .github/workflows/docker-dxspider-proxy.yml | 3 ++- .github/workflows/docker-iturhfprop-service.yml | 3 ++- .github/workflows/docker-openhamclock.yml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-dxspider-proxy.yml b/.github/workflows/docker-dxspider-proxy.yml index ae7b2a35..1a7d50b0 100644 --- a/.github/workflows/docker-dxspider-proxy.yml +++ b/.github/workflows/docker-dxspider-proxy.yml @@ -71,7 +71,7 @@ jobs: uses: docker/build-push-action@v7 with: context: . - push: true + push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} @@ -82,6 +82,7 @@ jobs: sbom: true - name: Generate artifact attestation + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} uses: actions/attest-build-provenance@v4 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} diff --git a/.github/workflows/docker-iturhfprop-service.yml b/.github/workflows/docker-iturhfprop-service.yml index 162e6072..d9ca339e 100644 --- a/.github/workflows/docker-iturhfprop-service.yml +++ b/.github/workflows/docker-iturhfprop-service.yml @@ -71,7 +71,7 @@ jobs: uses: docker/build-push-action@v7 with: context: . - push: true + push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} @@ -82,6 +82,7 @@ jobs: sbom: true - name: Generate artifact attestation + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} uses: actions/attest-build-provenance@v4 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} diff --git a/.github/workflows/docker-openhamclock.yml b/.github/workflows/docker-openhamclock.yml index 07d53895..05a8f24e 100644 --- a/.github/workflows/docker-openhamclock.yml +++ b/.github/workflows/docker-openhamclock.yml @@ -71,7 +71,7 @@ jobs: uses: docker/build-push-action@v7 with: context: . - push: true + push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} @@ -82,6 +82,7 @@ jobs: sbom: true - name: Generate artifact attestation + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} uses: actions/attest-build-provenance@v4 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}