# SvxPortal web — installation HOWTO This is the **web front-end only** for a SvxReflector: node map, monitor, last-heard, talkgroup list, user accounts and the reflector admin pages. It is plain HTML/JavaScript plus a small PHP backend that stores accounts in Redis. No build step, no framework, no composer. 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); `install/svxreflector.conf.example` is for that build | 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. --- ## 1. Packages ```bash sudo apt install nginx php8.4-fpm php8.4-redis php8.4-mbstring php8.4-cli redis-server certbot openssl ``` Check the PHP version you actually got (`php -v`) and use that number instead of `8.4` in the file names below. ## 2. Copy the web files ```bash sudo mkdir -p /var/www/html/svxportal sudo rsync -a --exclude .git --exclude install --exclude INSTALL.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 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 ``/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 API — what this repository does NOT contain 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 service is **not included** here. Without it the account/admin/talkgroup pages work, but the front page's node list, map, monitor, last-heard and trunks views simply stay **empty**. There is no error banner: the API helper returns null on a failed fetch and the page keeps its empty tables. Check the browser console (404 on `/svxportal/api/status`) if you are unsure. Endpoints the UI expects: | Path | Used by | |------|---------| | `/svxportal/api/status` | node table, topology | | `/svxportal/api/stream` | live updates (SSE) | | `/svxportal/api/lastheard` | last-heard list | | `/svxportal/api/monitor-nodes` | monitor view | | `/svxportal/api/local-trunks` | trunks view | You need to provide something that serves these, for example a small service that reads your reflector's `HTTP_SRV_PORT` status endpoint ([GeuReflector](https://github.com/audric/GeuReflector) serves `/status` there) and republishes it. The static reflector status page in `help.html` describes the fields. ## 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.