summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSt33v <github@f3rr3t.com>2026-06-09 13:04:04 +1000
committerSt33v <github@f3rr3t.com>2026-06-09 13:04:04 +1000
commitf058e83da43b0b661b45a7bd4c82b49c57e61d93 (patch)
treeeffc21ef57b00b45c8caa29424772f7fbbde03d8
parentc132b3985e24e557549544a1d10fb0daababdfb1 (diff)
Add IDR713 radar fetch and APNG loop
radarFetch.sh fetches BOM IDR713 transparencies (24h cache) and the last six 6-minute echo frames, composites them via cached lower/upper plates, and publishes an APNG loop to /srv/www/radar/. systemd: radar.{service,timer} on a 6-min cadence, with retry units mirroring the synoptic pattern. nginx: new radar.pestrel.com vhost (still commented in setup.sh until DNS propagation is confirmed). setup.sh provisions radar dirs, installs radar units, enables timer. deploy.sh accepts synoptic|radar arg. Parameterised on product code; adding another radar is a one-line config change. Spec: doc/bom-radar-spec.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
-rwxr-xr-xdeploy.sh40
-rw-r--r--doc/bom-radar-spec.md249
-rw-r--r--nginx/radar.pestrel.com.conf23
-rw-r--r--radar.index.html12
-rwxr-xr-xradarFetch.sh190
-rwxr-xr-xsetup.sh24
-rw-r--r--systemd/radar-retry.service12
-rw-r--r--systemd/radar-retry.timer8
-rw-r--r--systemd/radar.service13
-rw-r--r--systemd/radar.timer11
10 files changed, 569 insertions, 13 deletions
diff --git a/deploy.sh b/deploy.sh
index 36d50e9..93c1026 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -4,13 +4,35 @@ set -euo pipefail
REMOTE=st33v@cremonde
PORT=40022
-echo "Deploying synopticChart.sh to ${REMOTE}..."
-scp -P "$PORT" synopticChart.sh "${REMOTE}:/opt/synoptic/synopticChart.sh"
+TARGET="${1:-synoptic}"
-echo "Restarting synoptic.service..."
-if ssh -t -p "$PORT" "$REMOTE" "sudo systemctl restart synoptic.service"; then
- echo "SUCCESS: synoptic.service ran cleanly."
-else
- echo "FAILURE: synoptic.service exited non-zero." >&2
- exit 1
-fi
+case "$TARGET" in
+ synoptic)
+ echo "Deploying synopticChart.sh to ${REMOTE}..."
+ scp -P "$PORT" synopticChart.sh "${REMOTE}:/opt/synoptic/synopticChart.sh"
+
+ echo "Restarting synoptic.service..."
+ if ssh -t -p "$PORT" "$REMOTE" "sudo systemctl restart synoptic.service"; then
+ echo "SUCCESS: synoptic.service ran cleanly."
+ else
+ echo "FAILURE: synoptic.service exited non-zero." >&2
+ exit 1
+ fi
+ ;;
+ radar)
+ echo "Deploying radarFetch.sh to ${REMOTE}..."
+ scp -P "$PORT" radarFetch.sh "${REMOTE}:/opt/radar/radarFetch.sh"
+
+ echo "Restarting radar.service..."
+ if ssh -t -p "$PORT" "$REMOTE" "sudo systemctl restart radar.service"; then
+ echo "SUCCESS: radar.service ran cleanly."
+ else
+ echo "FAILURE: radar.service exited non-zero." >&2
+ exit 1
+ fi
+ ;;
+ *)
+ echo "Usage: $0 [synoptic|radar]" >&2
+ exit 2
+ ;;
+esac
diff --git a/doc/bom-radar-spec.md b/doc/bom-radar-spec.md
new file mode 100644
index 0000000..3123a16
--- /dev/null
+++ b/doc/bom-radar-spec.md
@@ -0,0 +1,249 @@
+---
+title: "BOM Radar Display — Sydney (IDR713)"
+date-created: "2026-06-09"
+version: "0.1"
+type: "spec"
+author:
+ - "st33v"
+ - "claude-opus-4-8"
+model:
+ - "claude-opus-4-8"
+tldr: "Extend the existing bomsynoptic app to fetch, composite and serve Sydney radar (IDR713) from BOM anonymous FTP. Layer model: static transparencies (cached) plus a rolling buffer of the last six 6-minute rain-echo frames, served as a loopable sequence alongside the synoptic chart."
+status: "draft"
+spec-kind: "feature"
+target-repo: "bomsynoptic"
+target-machine: "cremonde"
+dependencies: []
+design-chat: "https://claude.ai/chat/..."
+phase: "1"
+phases-planned: 1
+implementing-agent: "claude-code"
+reader-targets:
+ - "claude-code"
+ - "st33v"
+tags:
+ - "bom"
+ - "radar"
+ - "ftp"
+---
+
+# BOM Radar Display — Sydney (IDR713)
+
+## 1. Purpose
+
+The `bomsynoptic` app currently fetches and serves the BOM MSLP synoptic
+chart on a six-hourly cadence. This spec adds a second product class:
+**rain radar**, beginning with the **128 km Sydney (Terrey Hills)** radar,
+product code **`IDR713`**.
+
+Unlike the synoptic chart — a single self-contained image — a radar view is a
+*composite* of several PNG layers that must be stacked in the correct order,
+and the rain-echo layer refreshes roughly every six minutes. The deliverable
+is the current radar image plus a short loop of the last half-dozen frames,
+served in the same manner as the existing synoptic chart.
+
+Brisbane (Mt Stapylton) and other radars are explicitly **out of scope** for
+this phase; the design must make duplication to a new product code trivial
+(§9).
+
+## 2. How BOM radar works (background for the implementer)
+
+BOM does not publish a single ready-made radar image. It publishes a set of
+**transparent PNG layers**, all sharing identical pixel dimensions, intended
+to be alpha-composited in a fixed order. Two distinct cadences apply:
+
+- **Static layers** (basemap, topography, range rings, place labels, feature
+ overlays, legend) change only rarely — when BOM revises the basemap. Fetch
+ once, cache, refresh occasionally (e.g. daily).
+- **Dynamic layers** (the rain-echo frames) are emitted **every ~6 minutes**.
+ Each frame is a separate timestamped file. Several recent frames are present
+ on the server at any moment; we take the most recent *N*.
+
+All files live under BOM's **anonymous FTP** service. Availability is
+best-effort — BOM explicitly does not guarantee it — so resilience matters
+(§8).
+
+## 3. FTP source
+
+Anonymous FTP, no credentials beyond user `anonymous` (empty / any password).
+
+| Layer class | Directory |
+|---|---|
+| Rain-echo frames (dynamic) | `ftp://ftp.bom.gov.au/anon/gen/radar/` |
+| Transparencies (static) | `ftp://ftp.bom.gov.au/anon/gen/radar_transparencies/` |
+
+### 3.1 Dynamic frames
+
+Filename pattern in `/anon/gen/radar/`:
+
+```
+IDR713.T.<YYYYMMDDHHMM>.png
+```
+
+- The timestamp is **UTC**.
+- Because the timestamp is fixed-width and big-endian, a plain lexical sort of
+ the matching filenames is also a chronological sort.
+- Selection: list the directory, filter to the `IDR713.T.` prefix, sort, take
+ the last **6** (configurable as `FRAME_COUNT`, default 6).
+
+### 3.2 Static transparencies
+
+Files in `/anon/gen/radar_transparencies/`, all prefixed `IDR713.`:
+
+| File | Role |
+|---|---|
+| `IDR713.background.png` | base map: land/sea, coastline |
+| `IDR713.topography.png` | terrain shading |
+| `IDR713.range.png` | range rings |
+| `IDR713.locations.png` | town / city labels |
+| `IDR713.roads.png` | roads (optional overlay) |
+| `IDR713.rail.png` | rail (optional overlay) |
+| `IDR713.waterways.png` | rivers (optional overlay) |
+| `IDR713.catchments.png` | catchment boundaries (optional) |
+| `IDR713.wthrDistricts.png`| weather district boundaries (optional) |
+| `IDR713.legend.0.png` | colour-scale legend |
+
+Not every transparency exists for every radar. The implementer should fetch
+the directory listing once and treat any missing optional layer as absent
+rather than failing.
+
+## 4. Compositing model
+
+Alpha-composite **bottom → top** in this order. Optional feature overlays sit
+between topography and the radar echoes or above them per taste; the
+constraint that matters is that **labels and range rings render on top of the
+rain** so they stay legible.
+
+```
+1. IDR713.background.png ← bottom (opaque base)
+2. IDR713.topography.png
+3. (optional) catchments / waterways / roads / rail / wthrDistricts
+4. IDR713.T.<timestamp>.png ← the rain echoes (per frame)
+5. IDR713.range.png
+6. IDR713.locations.png ← top (labels)
+```
+
+The **legend** (`IDR713.legend.0.png`) is a separate strip. Either composite
+it into a fixed corner/margin of the output, or serve it as a sibling asset
+the front-end positions beside the loop. Recommend compositing it in so the
+served image is self-describing.
+
+Implementation note: build the **static base** (layers 1–3 + the on-top
+overlays 5–6 are split around the echo, so cache them as two precomposited
+plates — a *lower plate* under the echo and an *upper plate* over it). For
+each frame: `lower_plate → echo → upper_plate`. This reduces per-frame work to
+two composites instead of seven.
+
+Suggested tooling: Pillow (`PIL.Image.alpha_composite`) keeps the dependency
+footprint small and matches the likely existing stack; ImageMagick is an
+acceptable alternative if already present.
+
+## 5. Fetch cadence & rolling buffer
+
+- **Echo frames:** poll every **6 minutes** (`POLL_INTERVAL`, default 360 s).
+ On each poll, fetch only frames not already held locally (compare against
+ the buffer). Maintain a rolling buffer of the last `FRAME_COUNT` (6) frames;
+ evict older raw frames and their composites.
+- **Transparencies:** refresh on startup and then daily (`TRANSPARENCY_TTL`,
+ default 24 h). Survive a failed refresh by keeping the cached plates.
+- Do not hammer the server: the 6-minute poll matches BOM's emission rate.
+ Skip a poll cleanly if the previous one is still running.
+
+Note the contrast with the existing synoptic job: synoptic is six-**hourly**;
+radar is six-**minute**. The scheduler must accommodate both — reuse the
+existing scheduling mechanism if it generalises, otherwise run radar on its
+own timer.
+
+## 6. Outputs
+
+Produce two things per cycle:
+
+1. **Latest still** — the most recent frame composited over the plates.
+ Stable filename (e.g. `idr713-latest.png`) so the served URL never
+ changes.
+2. **Loop** — the last 6 composited frames. Serve either as:
+ - an **animated APNG/GIF** (`idr713-loop.png` / `.gif`) — simplest for the
+ front-end, single asset, frame timing baked in; or
+ - a **numbered sequence** (`idr713.0.png … idr713.5.png`) plus a small JSON
+ manifest of frame timestamps, leaving animation to client-side JS.
+
+Recommend the **sequence + manifest** route: it mirrors how BOM's own viewer
+works, lets the front-end control frame rate and pausing, and avoids
+re-encoding a whole animation every six minutes. The manifest also lets the UI
+print the real observation time (converted from UTC to the user's / Sydney
+local time) under each frame.
+
+## 7. Storage layout (suggested)
+
+```
+<data-root>/radar/idr713/
+ transparencies/ # cached static PNGs, refreshed daily
+ frames/ # raw echo PNGs, rolling buffer of 6
+ out/
+ idr713-latest.png
+ idr713.0.png … idr713.5.png
+ manifest.json # [{seq, utc, local, src_filename}, …]
+```
+
+Mirror whatever convention the synoptic side already uses for its served
+directory; the above is a fallback if there's no established pattern.
+
+## 8. Resilience
+
+- **Missing / partial frames:** if a poll returns fewer than 6 frames, serve
+ what exists; never blank the display.
+- **FTP unreachable:** retain and keep serving the last good composites; log
+ and retry next cycle. BOM does not guarantee availability.
+- **Stale data:** stamp the manifest with the newest frame's UTC time so the
+ UI can grey out / warn if the latest echo is older than, say, 20 minutes.
+- **Corrupt download:** validate each PNG opens before it enters the buffer;
+ discard and retry otherwise.
+- **Dimension mismatch:** if a transparency and the echo frames disagree on
+ size (can happen across a BOM basemap revision), refetch transparencies
+ before compositing.
+
+## 9. Generalising to other radars
+
+Parameterise on the product code. A single config entry should be enough to
+add Brisbane (Mt Stapylton) later:
+
+```yaml
+radars:
+ - id: "IDR713" # Sydney (Terrey Hills) 128 km
+ enabled: true
+ - id: "IDR663" # Brisbane (Mt Stapylton) 128 km — confirm code before enabling
+ enabled: false
+```
+
+Everything else — directories, filename patterns, layer set, compositing
+order — is identical across radars, differing only by the `IDRnnn` stem.
+**Do not hardcode `IDR713`.** (The Brisbane code above is a placeholder;
+st33v will confirm the exact product before that radar is switched on.)
+
+## 10. Copyright / terms of use
+
+BOM anonymous-FTP products are licensed for personal or in-organisation use,
+**not** commercial use, and republishing is meant to go through Registered
+User services. Since `st33v.com` is a public-facing surface, this is worth a
+deliberate decision rather than an oversight. Options: attribute BOM clearly
+and treat the personal/non-commercial display as within bounds, gate the radar
+behind the internal (WireGuard) surface only, or register. **Flag for st33v —
+do not silently publish.**
+
+## 11. Out of scope (this phase)
+
+- Brisbane and all radars other than IDR713 (config-ready, disabled).
+- Doppler wind / velocity products and the rainfall-accumulation products.
+- Historical / archived frames beyond the rolling 6-frame buffer.
+- Any front-end beyond serving the still, the sequence, and the manifest in
+ the same idiom as the existing synoptic chart.
+
+## 12. Open questions for Claude Code
+
+1. Does the existing scheduler generalise to a 6-minute timer, or does radar
+ need its own?
+2. What's the established served-assets directory and URL pattern on the
+ synoptic side — reuse it.
+3. Pillow already a dependency, or introduce it?
+4. Loop-as-sequence (recommended) vs animated single asset — confirm against
+ the existing front-end's capabilities.
diff --git a/nginx/radar.pestrel.com.conf b/nginx/radar.pestrel.com.conf
new file mode 100644
index 0000000..b7d9c08
--- /dev/null
+++ b/nginx/radar.pestrel.com.conf
@@ -0,0 +1,23 @@
+# BOM radar loop — radar.pestrel.com
+# Installed to /etc/nginx/conf.d/radar.conf by setup.sh
+#
+# Requires /etc/nginx/nginx.conf to include:
+# include /etc/nginx/conf.d/*.conf;
+
+server {
+ listen 80;
+ listen [::]:80;
+ server_name radar.pestrel.com;
+
+ root /srv/www/radar;
+ index index.html;
+
+ # APNG refreshes every ~6 minutes; let clients revalidate often.
+ location ~ \.apng$ {
+ add_header Cache-Control "no-cache";
+ }
+
+ location / {
+ try_files $uri $uri/ =404;
+ }
+}
diff --git a/radar.index.html b/radar.index.html
new file mode 100644
index 0000000..63ecec6
--- /dev/null
+++ b/radar.index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Radar — Sydney</title>
+<style>
+ body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }
+ img { max-width: 100%; max-height: 100vh; }
+</style>
+</head>
+<body><img src="/idr713-loop.apng" alt="Sydney radar loop"></body>
+</html>
diff --git a/radarFetch.sh b/radarFetch.sh
new file mode 100755
index 0000000..0dcc457
--- /dev/null
+++ b/radarFetch.sh
@@ -0,0 +1,190 @@
+#!/bin/bash
+set -euo pipefail
+shopt -s nullglob
+
+# SJP 2026-06-09
+# Fetch BOM radar layers via anonymous FTP, alpha-composite them,
+# publish the last FRAME_COUNT frames as an APNG loop.
+# Parameterised on product code; default IDR713 (Sydney 128 km).
+# Deployed to /opt/radar/radarFetch.sh on cremonde for radar.pestrel.com.
+
+RADAR_ID="${1:-IDR713}"
+FRAME_COUNT="${FRAME_COUNT:-6}"
+FRAME_DELAY="${FRAME_DELAY:-50}" # centiseconds per frame
+END_PAUSE="${END_PAUSE:-150}" # centiseconds, pause on last frame
+TRANSPARENCY_TTL_HOURS="${TRANSPARENCY_TTL_HOURS:-24}"
+
+DATA_ROOT="/var/lib/radar/${RADAR_ID,,}"
+TRANS_DIR="${DATA_ROOT}/transparencies"
+FRAMES_DIR="${DATA_ROOT}/frames"
+PLATES_DIR="${DATA_ROOT}/plates"
+OUT_DIR="${DATA_ROOT}/out"
+PUBLISH_DIR="/srv/www/radar"
+PUBLISH_PATH="${PUBLISH_DIR}/${RADAR_ID,,}-loop.apng"
+
+FTP_DYNAMIC="ftp://ftp.bom.gov.au/anon/gen/radar/"
+FTP_STATIC="ftp://ftp.bom.gov.au/anon/gen/radar_transparencies/"
+
+require_cmd() {
+ command -v "$1" >/dev/null 2>&1 || { echo "Required command not found: $1" >&2; exit 127; }
+}
+require_cmd curl
+require_cmd magick
+
+mkdir -p "$TRANS_DIR" "$FRAMES_DIR" "$PLATES_DIR" "$OUT_DIR"
+
+fetch_validated() {
+ # $1 url $2 dest
+ local url="$1" dest="$2" tmp="${2}.tmp"
+ curl -q -fs -o "$tmp" "$url" || { rm -f "$tmp"; return 1; }
+ if magick identify "$tmp" >/dev/null 2>&1; then
+ mv "$tmp" "$dest"
+ else
+ rm -f "$tmp"
+ return 1
+ fi
+}
+
+# --- 1. Refresh transparencies if stale ----------------------------------
+TRANS_MARKER="${TRANS_DIR}/.last_refreshed"
+needs_refresh=1
+if [ -f "$TRANS_MARKER" ]; then
+ age_h=$(( ( $(date +%s) - $(stat -c %Y "$TRANS_MARKER") ) / 3600 ))
+ [ "$age_h" -lt "$TRANSPARENCY_TTL_HOURS" ] && needs_refresh=0
+fi
+
+if [ "$needs_refresh" -eq 1 ]; then
+ echo "Refreshing transparencies for ${RADAR_ID}..."
+ if listing=$(curl -q -fs "$FTP_STATIC"); then
+ # ls -l style listing; filename is the last whitespace-delimited field
+ while read -r fname; do
+ [ -n "$fname" ] || continue
+ fetch_validated "${FTP_STATIC}${fname}" "${TRANS_DIR}/${fname}" \
+ || echo " failed: ${fname}" >&2
+ done < <(echo "$listing" | awk '{print $NF}' | grep "^${RADAR_ID}\." || true)
+ touch "$TRANS_MARKER"
+ # invalidate precomposited plates
+ rm -f "${PLATES_DIR}/lower.png" "${PLATES_DIR}/upper.png"
+ else
+ echo "Transparency listing failed; keeping cached copies." >&2
+ fi
+fi
+
+# --- 2. Build precomposited plates ---------------------------------------
+BACKGROUND="${TRANS_DIR}/${RADAR_ID}.background.png"
+if [ ! -f "$BACKGROUND" ]; then
+ echo "No background transparency cached; cannot composite." >&2
+ exit 1
+fi
+
+existing_layers() {
+ # echo paths that exist, in given order
+ local base p
+ for base in "$@"; do
+ p="${TRANS_DIR}/${RADAR_ID}.${base}.png"
+ [ -f "$p" ] && echo "$p"
+ done
+}
+
+if [ ! -f "${PLATES_DIR}/lower.png" ]; then
+ # background (opaque) + topography + optional feature overlays under the echo
+ readarray -t lower_layers < <(existing_layers \
+ background topography catchments waterways wthrDistricts roads rail)
+ magick "${lower_layers[@]}" -background none -layers flatten "${PLATES_DIR}/lower.png"
+fi
+
+if [ ! -f "${PLATES_DIR}/upper.png" ]; then
+ # range rings + place labels go over the echo so they stay legible
+ readarray -t upper_layers < <(existing_layers range locations)
+ if [ "${#upper_layers[@]}" -gt 0 ]; then
+ magick "${upper_layers[@]}" -background none -layers flatten "${PLATES_DIR}/upper.png"
+ fi
+fi
+
+# --- 3. Determine current top-N frames on the FTP ------------------------
+echo "Listing dynamic frames..."
+if ! listing=$(curl -q -fs "$FTP_DYNAMIC"); then
+ echo "Dynamic listing failed; serving last good loop." >&2
+ exit 0
+fi
+
+mapfile -t remote_frames < <(echo "$listing" \
+ | awk '{print $NF}' \
+ | grep "^${RADAR_ID}\.T\.[0-9]\{12\}\.png$" \
+ | sort \
+ | tail -n "$FRAME_COUNT")
+
+if [ "${#remote_frames[@]}" -eq 0 ]; then
+ echo "No remote frames for ${RADAR_ID}; serving last good loop." >&2
+ exit 0
+fi
+
+# --- 4. Fetch missing frames; evict frames outside the rolling buffer ----
+for fname in "${remote_frames[@]}"; do
+ if [ ! -f "${FRAMES_DIR}/${fname}" ]; then
+ echo " fetching ${fname}"
+ fetch_validated "${FTP_DYNAMIC}${fname}" "${FRAMES_DIR}/${fname}" \
+ || echo " failed: ${fname}" >&2
+ fi
+done
+
+keep_set=" $(printf '%s ' "${remote_frames[@]}")"
+for existing in "$FRAMES_DIR"/${RADAR_ID}.T.*.png; do
+ bn=$(basename "$existing")
+ case "$keep_set" in
+ *" $bn "*) ;;
+ *) rm -f "$existing" ;;
+ esac
+done
+
+# --- 5. Composite each held frame ----------------------------------------
+LEGEND="${TRANS_DIR}/${RADAR_ID}.legend.0.png"
+HAVE_UPPER=0; [ -f "${PLATES_DIR}/upper.png" ] && HAVE_UPPER=1
+HAVE_LEGEND=0; [ -f "$LEGEND" ] && HAVE_LEGEND=1
+
+# clear stale composited frames
+rm -f "${OUT_DIR}"/frame.*.png
+
+composited=()
+seq=0
+for fname in "${remote_frames[@]}"; do
+ src="${FRAMES_DIR}/${fname}"
+ [ -f "$src" ] || continue
+ out="${OUT_DIR}/frame.$(printf '%02d' "$seq").png"
+
+ cmd=(magick "${PLATES_DIR}/lower.png" "$src" -composite)
+ [ "$HAVE_UPPER" -eq 1 ] && cmd+=("${PLATES_DIR}/upper.png" -composite)
+ [ "$HAVE_LEGEND" -eq 1 ] && cmd+=(-gravity southeast "$LEGEND" -composite +gravity)
+ cmd+=("$out")
+ "${cmd[@]}"
+
+ composited+=("$out")
+ seq=$((seq + 1))
+done
+
+if [ "${#composited[@]}" -eq 0 ]; then
+ echo "No frames composited; serving last good loop." >&2
+ exit 0
+fi
+
+# --- 6. Assemble APNG loop -----------------------------------------------
+# Per-frame delays: last frame gets END_PAUSE so the eye can rest on "now".
+mkdir -p "$PUBLISH_DIR"
+TMP_APNG="${OUT_DIR}/loop.apng.tmp"
+
+build_cmd=(magick -loop 0)
+last_idx=$(( ${#composited[@]} - 1 ))
+for i in "${!composited[@]}"; do
+ if [ "$i" -eq "$last_idx" ]; then
+ build_cmd+=(-delay "$END_PAUSE" "${composited[$i]}")
+ else
+ build_cmd+=(-delay "$FRAME_DELAY" "${composited[$i]}")
+ fi
+done
+build_cmd+=("$TMP_APNG")
+"${build_cmd[@]}"
+
+install -m 644 "$TMP_APNG" "$PUBLISH_PATH"
+rm -f "$TMP_APNG"
+
+echo "Published ${PUBLISH_PATH} (frames=${#composited[@]}, latest=${remote_frames[-1]})"
diff --git a/setup.sh b/setup.sh
index f2707d9..363a6fc 100755
--- a/setup.sh
+++ b/setup.sh
@@ -30,6 +30,11 @@ if grep -rl "server_name.*pestrel\.com" /etc/nginx/ 2>/dev/null; then
CONFLICT=1
fi
+if grep -rl "server_name.*radar\.pestrel\.com" /etc/nginx/ 2>/dev/null; then
+ echo "WARNING: The above nginx config(s) already reference radar.pestrel.com — check for duplicate server blocks."
+ CONFLICT=1
+fi
+
if grep -rl "listen.*80.*default_server" /etc/nginx/ 2>/dev/null; then
echo "WARNING: The above nginx config(s) define a default_server on port 80 — may intercept traffic intended for this site."
CONFLICT=1
@@ -55,30 +60,41 @@ install -d /srv/www
install -d -o "$OWNER" -g "$OWNER" /srv/www/pestrel
chown "$OWNER:$OWNER" /srv/www/pestrel
+install -d -o "$OWNER" -g "$OWNER" /opt/radar
+install -d -o "$OWNER" -g "$OWNER" /var/lib/radar
+install -d -o "$OWNER" -g "$OWNER" /srv/www/radar
+
# ---------------------------------------------------------------------------
# Web content
# ---------------------------------------------------------------------------
echo "==> Writing index.html..."
install -o "$OWNER" -g "$OWNER" -m 644 "$SCRIPT_DIR/index.html" /srv/www/pestrel/index.html
+install -o "$OWNER" -g "$OWNER" -m 644 "$SCRIPT_DIR/radar.index.html" /srv/www/radar/index.html
+
+echo "==> Installing radarFetch.sh..."
+install -o "$OWNER" -g "$OWNER" -m 755 "$SCRIPT_DIR/radarFetch.sh" /opt/radar/radarFetch.sh
# ---------------------------------------------------------------------------
# Systemd units
# ---------------------------------------------------------------------------
echo "==> Installing systemd unit files..."
-for unit in synoptic.service synoptic.timer synoptic-retry.service synoptic-retry.timer; do
+for unit in synoptic.service synoptic.timer synoptic-retry.service synoptic-retry.timer \
+ radar.service radar.timer radar-retry.service radar-retry.timer; do
install -m 644 "$SCRIPT_DIR/systemd/${unit}" "/etc/systemd/system/${unit}"
echo " installed ${unit}"
done
-echo "==> Reloading systemd and enabling timer..."
+echo "==> Reloading systemd and enabling timers..."
systemctl daemon-reload
systemctl enable --now synoptic.timer
+systemctl enable --now radar.timer
# ---------------------------------------------------------------------------
# Nginx
# ---------------------------------------------------------------------------
-#echo "==> Installing nginx config..."
-#install -m 644 "$SCRIPT_DIR/nginx/pestrel.com.conf" /etc/nginx/conf.d/synoptic.conf
+#echo "==> Installing nginx configs..."
+#install -m 644 "$SCRIPT_DIR/nginx/pestrel.com.conf" /etc/nginx/conf.d/synoptic.conf
+#install -m 644 "$SCRIPT_DIR/nginx/radar.pestrel.com.conf" /etc/nginx/conf.d/radar.conf
#echo "==> Testing nginx config..."
#nginx -t
diff --git a/systemd/radar-retry.service b/systemd/radar-retry.service
new file mode 100644
index 0000000..b878303
--- /dev/null
+++ b/systemd/radar-retry.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Retry BOM radar fetch (after failure)
+
+[Service]
+Type=oneshot
+User=st33v
+WorkingDirectory=/var/lib/radar
+ExecStart=/opt/radar/radarFetch.sh
+ExecStopPost=/bin/sh -c 'STATUS=SUCCESS; [ "$$EXIT_STATUS" != "0" ] && STATUS=FAILURE; logger -t radar-retry -p user.err "radar-retry $$STATUS exit=$$EXIT_STATUS"'
+SyslogIdentifier=radar-retry
+StandardOutput=journal
+StandardError=journal
diff --git a/systemd/radar-retry.timer b/systemd/radar-retry.timer
new file mode 100644
index 0000000..d7c8d9b
--- /dev/null
+++ b/systemd/radar-retry.timer
@@ -0,0 +1,8 @@
+[Unit]
+Description=Retry radar fetch 2 minutes after failure
+
+[Timer]
+OnActiveSec=2min
+
+[Install]
+WantedBy=timers.target
diff --git a/systemd/radar.service b/systemd/radar.service
new file mode 100644
index 0000000..8e9dd40
--- /dev/null
+++ b/systemd/radar.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Fetch BOM radar and publish loop APNG
+OnFailure=radar-retry.timer
+
+[Service]
+Type=oneshot
+User=st33v
+WorkingDirectory=/var/lib/radar
+ExecStart=/opt/radar/radarFetch.sh
+ExecStopPost=/bin/sh -c 'STATUS=SUCCESS; [ "$$EXIT_STATUS" != "0" ] && STATUS=FAILURE; logger -t radar -p user.err "radar $$STATUS exit=$$EXIT_STATUS"'
+SyslogIdentifier=radar
+StandardOutput=journal
+StandardError=journal
diff --git a/systemd/radar.timer b/systemd/radar.timer
new file mode 100644
index 0000000..c147481
--- /dev/null
+++ b/systemd/radar.timer
@@ -0,0 +1,11 @@
+[Unit]
+Description=Run BOM radar fetch every 6 minutes
+
+[Timer]
+OnBootSec=2min
+OnUnitActiveSec=6min
+AccuracySec=15s
+Persistent=true
+
+[Install]
+WantedBy=timers.target