install/systemd: tglist-sync timer for follower boxes; INSTALL.md notes on systemd/cron

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 23:33:48 +02:00
co-authored by Claude Fable 5.1
parent af1793a742
commit d18a0f0b91
5 changed files with 96 additions and 0 deletions
+28
View File
@@ -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
@@ -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
@@ -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
@@ -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
+38
View File
@@ -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"