diff --git a/INSTALL.md b/INSTALL.md index 222eef6..db4630b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -144,6 +144,34 @@ sudo systemd-tmpfiles --create /etc/tmpfiles.d/svxportal-tglist.conf This creates `/etc/svxlink/tglist.json` owned by `www-data`. The path is `tglist_file` in `config.php`; change both if you want it elsewhere. +### 6b. Optional: follower box that mirrors another portal's talkgroup list + +If you run several reflectors and want ONE talkgroup list, one portal is the +master (its list is edited in the web) and the others pull it four times a day +with a systemd timer. Nothing but `curl` and `python3` is involved. + +```bash +sudo cp install/systemd/svxportal-tglist-sync.service install/systemd/svxportal-tglist-sync.timer /etc/systemd/system/ +sudo cp install/systemd/tglist-sync.sh /etc/svxlink/tglist-sync.sh && sudo chmod 755 /etc/svxlink/tglist-sync.sh +sudo cp install/systemd/default-svxportal-tglist-sync /etc/default/svxportal-tglist-sync +sudoedit /etc/default/svxportal-tglist-sync # UPSTREAM = the master's https://.../tglist.php +sudo systemctl daemon-reload +sudo systemctl enable --now svxportal-tglist-sync.timer +sudo systemctl start svxportal-tglist-sync.service && journalctl -u svxportal-tglist-sync -n 3 +``` + +On the master, or on a standalone box, do **not** install this (or leave +`UPSTREAM` empty; the script then exits without touching the file). + +Other systemd notes: + +- `svxreflector.service` is used **unmodified** from the SvxLink package. No + drop-in is needed for the portal. +- Redis runs from Debian's `redis-server@.service` template (step 3); no unit + file to add. +- There are no cron jobs. PHP session cleanup is Debian's stock + `phpsessionclean.timer`. + ## 7. nginx + TLS ```bash diff --git a/install/systemd/default-svxportal-tglist-sync b/install/systemd/default-svxportal-tglist-sync new file mode 100644 index 0000000..95b70bb --- /dev/null +++ b/install/systemd/default-svxportal-tglist-sync @@ -0,0 +1,8 @@ +# Follower-box TG list sync (svxportal-tglist-sync.service/.timer). +# +# UPSTREAM = the master's public TG list URL. When set, this box is a FOLLOWER: +# the timer pulls that list into DEST 4x/day and the web TG editor is hidden. +# Leave UPSTREAM empty on the MASTER and on fully-standalone boxes — they own the +# list locally and edit it in the web; the sync is then a no-op. +UPSTREAM=https://svxportal.my.domain.com/tglist.php +DEST=/etc/svxlink/tglist.json diff --git a/install/systemd/svxportal-tglist-sync.service b/install/systemd/svxportal-tglist-sync.service new file mode 100644 index 0000000..6271ba7 --- /dev/null +++ b/install/systemd/svxportal-tglist-sync.service @@ -0,0 +1,10 @@ +[Unit] +Description=Sync canonical TG list from svxportal master +Documentation=file:/etc/svxlink/tglist-sync.sh +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +EnvironmentFile=/etc/default/svxportal-tglist-sync +ExecStart=/bin/bash /etc/svxlink/tglist-sync.sh diff --git a/install/systemd/svxportal-tglist-sync.timer b/install/systemd/svxportal-tglist-sync.timer new file mode 100644 index 0000000..1ec27c5 --- /dev/null +++ b/install/systemd/svxportal-tglist-sync.timer @@ -0,0 +1,12 @@ +[Unit] +Description=Sync canonical TG list from svxportal master (4x/day) + +[Timer] +# Shortly after boot, then every 6 hours (00,06,12,18). Persistent catches a +# missed run if the box was off at the scheduled time. +OnBootSec=2min +OnCalendar=*-*-* 00,06,12,18:00:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/install/systemd/tglist-sync.sh b/install/systemd/tglist-sync.sh new file mode 100644 index 0000000..b85217d --- /dev/null +++ b/install/systemd/tglist-sync.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Follower-box sync of the canonical TG list. +# +# Pulls the master list from the svxportal master (its public /tglist.php) into +# the LOCAL /etc/svxlink/tglist.json, so every local program reads ONE identical +# list: svxdr (Lastheard TG name/Location) and the box's own /tglist.php (TG +# Monitor) both read that local file. No extra daemon — a plain curl run by a +# systemd timer. +# +# Master and fully-standalone boxes OWN the file locally (web editor) and must +# NOT run this — leave UPSTREAM empty there (the timer is a no-op / not enabled). +set -euo pipefail +: "${UPSTREAM:=}" +: "${DEST:=/etc/svxlink/tglist.json}" + +# Empty UPSTREAM = this box owns its list; nothing to sync. +[ -n "$UPSTREAM" ] || { echo "UPSTREAM empty — this box owns its TG list, skipping sync"; exit 0; } + +tmp="$(mktemp)"; out="$(mktemp)" +trap 'rm -f "$tmp" "$out"' EXIT + +curl -fsS -m 20 "$UPSTREAM" -o "$tmp" + +# Keep ONLY the canonical shape {tgs:[...],requests:[]}. Refuse to overwrite the +# live file with anything that is not a valid list (a 404/HTML/partial body must +# never blank the TG names) — last good copy then simply stays. +python3 - "$tmp" "$out" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +tgs = d.get("tgs") +if not isinstance(tgs, list) or not tgs: + raise SystemExit("upstream has no tgs list — refusing to overwrite") +json.dump({"tgs": tgs, "requests": []}, open(sys.argv[2], "w"), indent=2) +PY + +# www-data owns it so the web can still read it; atomic replace via install. +install -o www-data -g www-data -m 0664 "$out" "$DEST" +echo "TG list synced from $UPSTREAM -> $DEST"