Files
reflector-dashboard/INSTALL.md
T

401 lines
16 KiB
Markdown

# SvxPortal — installation HOWTO
SvxPortal 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](https://github.com/audric/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
```bash
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:
```bash
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
```bash
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)
```bash
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:
```bash
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:
```bash
sudo usermod -aG redis www-data
```
Test:
```bash
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.
```bash
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:
```ini
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:
```bash
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`)
```bash
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:
```php
'mail' => [
'from' => 'no-reply@example.com',
'from_name' => 'SvxPortal',
'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:
```bash
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.
```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
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):
```bash
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.
```bash
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:
```bash
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.
```bash
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
```bash
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
```bash
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.