Files
reflector-dashboard/INSTALL.md
T

16 KiB

Reflector Dashboard — installation HOWTO

Reflector Dashboard is the web front-end for a SvxReflector: node map, monitor, last-heard, talkgroup list, user accounts and reflector admin pages. It has two parts, both in this repository:

  • the web: plain HTML/JavaScript plus a small PHP backend that keeps accounts in Redis. No build step, no framework, no composer.
  • svxdr: a Go daemon that reads the reflector's /status and serves the live data the dashboard shows. One go build, no other build tools.

How the pieces connect:

browser ──HTTPS──> nginx ──> static files + PHP-FPM ──> Redis   (accounts, sessions, bans)
                     │
                     └── /svxportal/api/ ──> svxdr nodelist :8098
                                                  └──> svxdr proxy :8090 ──> svxreflector HTTP_SRV_PORT :8099

Steps in order: 0 get the code, 1 packages, 2 web files, 3 Redis, 4 PHP-FPM, 5 web config, 6 talkgroup file, 7 nginx + TLS, 8 first admin, 9 svxdr. Step 10 is optional. Step 11 is troubleshooting.

What you need on the server:

Component Why
nginx serves the static files and passes *.php to PHP-FPM
PHP 8.4 with FPM the auth/ backend (login, accounts, admin API). Developed and tested on 8.4
PHP extensions: redis, mbstring, openssl, json, session used by auth/lib.php
Redis 6+ stores accounts, sessions, bans, talkgroup requests
A way to send e-mail signup verification and password reset (local MTA or an SMTP relay, see step 5)
TLS certificate the session cookie is Secure; login does not work over plain HTTP
A running SvxReflector we use GeuReflector (SvxReflector with trunking); build it per its own README. install/svxreflector.conf.example is for that build
Go 1.24+ to compile svxdr (step 9). Debian 13 ships it; on Debian 12 / Ubuntu 24.04 the packaged Go is older but fetches the right toolchain itself, see step 9

Everything below assumes Debian 12/13 or Ubuntu 24.04, PHP 8.4 and that the site will live at https://svxportal.example.com. Replace the host name everywhere you see it.


0. Get the code

sudo apt install git
cd ~ && git clone https://git.sa7aux.se/bjorne/svx-web-pub.git svxportal
cd svxportal

All paths below are relative to this checkout unless they start with /.

1. Packages

Everything the web, Redis, TLS and svxdr need, in one go:

sudo apt install nginx php8.4-fpm php8.4-redis php8.4-mbstring php8.4-cli \
                 redis-server certbot openssl rsync curl python3 sudo golang-go
Package Used by
nginx web server
php8.4-fpm, php8.4-redis, php8.4-mbstring, php8.4-cli PHP backend; -cli for svx-mkadmin.php (step 8). openssl/json/session are built into Debian's PHP
redis-server account store; also provides the redis-server@ instance template (step 3)
certbot Let's Encrypt certificate (step 7)
openssl CSR parsing in the certificate admin pages, password generation
rsync copying the web files into the webroot (step 2)
curl, python3 the optional talkgroup sync timer (step 6b) and the checks in this guide
golang-go compiles svxdr (step 9)

Check the PHP version you actually got (php -v) and use that number instead of 8.4 in the file names below. On Debian 12 the PHP is 8.2; the portal is developed on 8.4, so prefer Debian 13 or the packages.sury.org PHP repo.

2. Copy the web files

sudo mkdir -p /var/www/html/svxportal
sudo rsync -a --exclude .git --exclude install --exclude svxdr --exclude INSTALL.md --exclude README.md ./ /var/www/html/svxportal/
sudo chown -R www-data:www-data /var/www/html/svxportal
sudo find /var/www/html/svxportal -type d -exec chmod 2775 {} +
sudo find /var/www/html/svxportal -type f -exec chmod 0644 {} +

3. Redis (dedicated instance, unix socket)

sudo cp install/redis-svxportal.conf /etc/redis/redis-svxportal.conf
sudo mkdir -p /var/lib/redis-svxportal
sudo chown redis:redis /var/lib/redis-svxportal /etc/redis/redis-svxportal.conf
sudo chmod 640 /etc/redis/redis-svxportal.conf

Generate a password and put it in the config:

openssl rand -hex 32
sudo sed -i "s/^requirepass .*/requirepass PASTE-THE-VALUE-HERE/" /etc/redis/redis-svxportal.conf
sudo systemctl enable --now redis-server@svxportal

The socket is created with mode 770 and group redis, so the web user must be in that group:

sudo usermod -aG redis www-data

Test:

redis-cli -s /run/redis-svxportal/redis.sock -a 'PASTE-THE-VALUE-HERE' ping   # -> PONG

4. PHP-FPM

Two pools are used. The default pool (www) handles all normal pages. A second small pool handles only auth/admin_reflector_api.php, so its longer timeout and separate process limits do not affect the rest of the site.

sudo cp install/php-fpm-svxadmin.conf /etc/php/8.4/fpm/pool.d/svxadmin.conf

Hardening of the default pool is recommended. Edit /etc/php/8.4/fpm/pool.d/www.conf and add at the end:

php_admin_value[disable_functions] = passthru,shell_exec,system,popen,proc_open,proc_close,pcntl_exec,dl,phpinfo,show_source
php_admin_value[expose_php] = Off

Do not add exec to that list. The portal uses exec() in the default pool too: openssl req to read the e-mail out of a pending CSR, and the sudo helpers from the node pages (auth/node_auth_api.php, auth/node_cmd_api.php, auth/admin_nodes.php). With exec disabled those pages return errors.

Then:

sudo systemctl restart php8.4-fpm
ls -l /run/php/   # both php8.4-fpm.sock and php8.4-fpm-svxadmin.sock must exist

5. Web app config (auth/config.php)

cd /var/www/html/svxportal
sudo -u www-data cp auth/config.php.example auth/config.php
sudo chmod 640 auth/config.php
sudoedit auth/config.php

Set:

  • redis.unix_socket -> /run/redis-svxportal/redis.sock
  • redis.password -> the value from step 3
  • base_url -> https://svxportal.example.com (used in the e-mail links)
  • mail.from -> the sender address for verification / reset mails
  • mqtt.node_broker_host / mqtt.node_broker_port -> the MQTT broker address shown to node owners on the node pages (your public TLS port, usually 8883). Without these the node MQTT page returns mqtt_broker_not_configured.

E-mail: with only mail.from set, PHP's mail() is used, which needs a working local MTA (/usr/sbin/sendmail, e.g. msmtp-mta or postfix). If you do not have one, add an SMTP relay instead and no MTA is needed:

'mail' => [
  'from'      => 'no-reply@example.com',
  'from_name' => 'Reflector Dashboard',
  'host'      => 'smtp.example.com',   // SSL SMTP (implicit TLS)
  'port'      => 465,
  'user'      => 'smtp-user',
  'pass'      => 'smtp-password',
],

config.php is listed in .gitignore and is never served by nginx (all *.php go to PHP-FPM, which just executes return [...]). Keep it mode 640.

6. Talkgroup list file

The talkgroup list is a JSON file the portal reads and writes:

sudo cp install/tmpfiles-svxportal-tglist.conf /etc/tmpfiles.d/svxportal-tglist.conf
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.

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

sudo mkdir -p /var/www/letsencrypt
sudo cp install/nginx-svxportal.conf /etc/nginx/sites-available/svxportal
sudo sed -i 's/svxportal\.example\.com/YOUR.HOST.NAME/g' /etc/nginx/sites-available/svxportal

Get a certificate first (the config references it):

sudo certbot certonly --webroot -w /var/www/letsencrypt -d YOUR.HOST.NAME

If certbot has not created /etc/letsencrypt/options-ssl-nginx.conf and ssl-dhparams.pem, run sudo certbot --nginx once or remove those two lines.

sudo ln -s /etc/nginx/sites-available/svxportal /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Open https://YOUR.HOST.NAME/ — the page should render. Click Sign in and you should get a proper "wrong password" style error, not a 500. A 500 here almost always means Redis (wrong socket/password, or www-data not in group redis).

Also edit these files in the web root for your own site name: robots.txt, sitemap.xml and the <title>/meta tags at the top of index.html.

8. First admin account

Accounts normally self-register and verify by e-mail. The first admin is created from the command line with the bundled script; it reuses the portal's own library, so it writes exactly what signup.php would:

cd /var/www/html/svxportal
sudo -u www-data php svx-mkadmin.php

Answer the prompts (callsign, e-mail, password, admin level). Levels:

Level Meaning
1 normal user
2 admin: accounts, nodes, certificates, talkgroups
3 super admin: may also see reflector/MQTT secrets and manage other admins

Log in on the web page with that callsign. The Admin menu appears.

9. Live data backend: svxdr

The dashboard (node list, map, monitor, last-heard, trunks) fetches JSON and Server-Sent Events from /svxportal/api/..., which nginx proxies to 127.0.0.1:8098. That backend is svxdr, included in svxdr/: one Go binary with four modules switched on or off per box in /etc/svxlink/svxdr.json.

Module What it does A plain reflector needs it?
proxy reads the reflector's own /status, caches it, serves it enriched on 8090 yes
nodelist builds the node/lastheard/monitor/trunk data from the proxy + Redis, serves /api/* on 8098 yes
astmeta AllStar/Asterisk AMI talker metadata for USRP bridges no, leave false
xlink relays talker events to an MQTT broker no, leave false

Without svxdr the account/admin/talkgroup pages work, but the front page's live views stay empty with no error banner (404 on /svxportal/api/status in the browser console).

Build

svxdr/go.mod asks for Go 1.24. Debian 13 ships 1.24. Debian 12 and Ubuntu 24.04 ship an older golang-go, but since Go 1.21 the go command downloads the required toolchain by itself on first build, so the packaged Go works as long as the box has internet access. The build also downloads the two Go dependencies (go-redis, paho-mqtt) from proxy.golang.org.

go version                          # any 1.21+ is fine, see above
cd svxdr && CGO_ENABLED=0 go build -trimpath -o svxdr . && cd ..
ls -l svxdr/svxdr                   # ~12 MB static binary
sudo install -m0755 svxdr/svxdr /usr/local/bin/svxdr

No internet on the server? Build on any machine with Go (same command, add GOOS=linux GOARCH=amd64 or arm64) and copy the single binary over.

If go build complains about the toolchain, install Go from https://go.dev/dl/ instead of the distro package and rerun.

Configure

sudo cp install/svxdr.json.example /etc/svxlink/svxdr.json
sudo chmod 640 /etc/svxlink/svxdr.json
sudoedit /etc/svxlink/svxdr.json

Set at least:

  • redis.socket and redis.password -> the same Redis as auth/config.php (step 3).
  • proxy.reflector_status_url -> your reflector's HTTP_SRV_PORT status URL. With HTTP_SRV_PORT=8099 in svxreflector.conf (as in install/svxreflector.conf.example) the default http://127.0.0.1:8099/status is right.
  • nodelist.reflector_status_url stays on the proxy (http://127.0.0.1:8090/status).
  • All CHANGEME tokens: any random string; the ingest tokens only matter if a remote box posts talker metadata to you.
  • nodelist.federation.seeds: /status URLs of other reflectors you want in the map that your own trunks do not reach. Empty = only what the crawl finds. aggregate_url empty disables the gap detector.

The port layout must match the nginx config: reflector 8099 -> proxy 8090 -> nodelist 8098 -> nginx /svxportal/api/.

Run

sudo cp install/systemd/svxdr.service /etc/systemd/system/svxdr.service
sudo systemctl daemon-reload
sudo systemctl enable --now svxdr
systemctl status svxdr --no-pager
curl -s http://127.0.0.1:8098/healthz          # -> ok
curl -s http://127.0.0.1:8098/api/status | head -c 300

Config changes are picked up with sudo systemctl reload svxdr (SIGHUP), no restart needed. journalctl -u svxdr -f shows what it polls and serves.

Endpoints the UI uses:

Path Used by
/svxportal/api/status node table, topology, CTCSS
/svxportal/api/stream live updates (SSE)
/svxportal/api/lastheard last-heard list
/svxportal/api/monitor-nodes monitor view
/svxportal/api/local-trunks trunks view

10. Reflector admin features (optional, need sudo helpers)

The pages Admin → Reflector, Certificates, Node control and MQTT talk to the running svxreflector on the same machine. They do it through small root-owned helper scripts called with sudo from auth/admin_reflector_api.php, auth/node_cmd_api.php and auth/node_auth_api.php:

Helper (expected in /usr/local/sbin/) Purpose
svxreflector-pty writes NODE BLOCK/UNBLOCK and CA commands to the reflector's COMMAND_PTY
svxreflector-systemctl restart/reload the svxreflector service
svxreflector-users add/remove [USERS]/[PASSWORDS] entries in svxreflector.conf
svx-mqtt-provision create/remove per-node MQTT accounts

Those helpers and their /etc/sudoers.d/ rules are not part of this repository; they are specific to how each reflector box is set up. Write your own for your environment (validate the callsign argument strictly, allow www-data to run only that one script with NOPASSWD), or skip this: all other pages work without them and the admin API returns a clear error when a helper is missing.

The certificate pages additionally read /etc/svxlink/svxreflector.conf to find CERT_PKI_DIR and watch pending_csrs/ in it, so www-data needs read access to the conf and the PKI directories (never to the private keys).

install/svxreflector.conf.example shows a complete reflector config as used with this portal: PKI/CA paths, COMMAND_PTY, HTTP_SRV_PORT, the [USERS]/[PASSWORDS] layout the helper edits, MQTT, and trunk/satellite sections. All secrets and hostnames in it are placeholders.

11. Checklist when something does not work

  • 500 on any auth/*.php: sudo tail -f /var/log/nginx/svxportal-error.log and journalctl -u php8.4-fpm. Usually Redis auth or missing php-redis.
  • "Not connected" banner, empty node list: no backend on 127.0.0.1:8098 (step 9).
  • No verification e-mail: mail() without an MTA silently fails. Configure the SMTP relay (step 5) and check journalctl -u php8.4-fpm for smtp: lines.
  • Login works but is lost on next page: site not on HTTPS (cookie is Secure) or base_url wrong.
  • Admin API says "exec disabled": admin_reflector_api.php was served by the default pool. Check the nginx location = block and that php8.4-fpm-svxadmin.sock exists.