341dae1913
Runs tssbot-web, tssbot-webhook, and tssbot-backend as systemd --user units instead of PM2 processes. tssbot-web moves from a 2-worker PM2 cluster to a single instance, so deploys now restart it directly instead of doing a zero-downtime cluster reload. webhook.cjs now shells out to `systemctl --user restart` instead of `pm2 reload`, and PM2_RESTART_TARGETS/WEBHOOK_PM2_NAME are renamed to RESTART_TARGETS/WEBHOOK_SERVICE_NAME. scripts/install-systemd-services.sh symlinks the new unit files into ~/.config/systemd/user and enables them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
284 lines
10 KiB
Markdown
284 lines
10 KiB
Markdown
# tssbot.web
|
|
|
|
Toothless' TSS Bot web stack.
|
|
|
|
The repo is split into:
|
|
|
|
- `frontend/` - React + Vite + Tailwind v4 web shell
|
|
- `backend/` - backend API service scaffold, ready for database-backed routes
|
|
- root process files - production frontend server, deploy webhook, systemd unit files, and shared repo scripts
|
|
|
|
Routes:
|
|
|
|
- `/` landing page
|
|
- `/teams` TSS team leaderboard
|
|
- `/teams/:teamname` generated team profile with roster, summary, rating history, and battle results
|
|
- `/battle-logs` Battle Logs
|
|
- `/viewers` public consented viewer analytics dashboard
|
|
|
|
## Local development
|
|
|
|
```sh
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
The frontend development server runs on <http://localhost:3001>.
|
|
|
|
By default, `/api/*` and `/health` requests are proxied to `http://localhost:6000`. Override
|
|
that with `VITE_API_TARGET`:
|
|
|
|
```sh
|
|
VITE_API_TARGET=http://localhost:8080 npm run dev
|
|
```
|
|
|
|
Run the Rust backend separately:
|
|
|
|
```sh
|
|
npm run dev:backend
|
|
```
|
|
|
|
The backend listens on <http://127.0.0.1:6000> by default and reads the SQLite
|
|
databases configured by `TSS_BATTLES_DB` and `TSS_TEAMS_DB`. Keep it bound to
|
|
`127.0.0.1` in production and let `tssbot-web` proxy public API requests.
|
|
|
|
## Production with systemd
|
|
|
|
On a fresh headless Ubuntu server, install the native build tools Rust crates
|
|
need before the first backend build:
|
|
|
|
```sh
|
|
sudo apt update
|
|
sudo apt install -y build-essential pkg-config curl
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
. "$HOME/.cargo/env"
|
|
```
|
|
|
|
```sh
|
|
npm install
|
|
npm run build
|
|
npm run build:backend
|
|
scripts/install-systemd-services.sh
|
|
```
|
|
|
|
`scripts/install-systemd-services.sh` symlinks the unit files under `systemd/`
|
|
into `~/.config/systemd/user/`, then runs `systemctl --user daemon-reload` and
|
|
`enable --now` for all three services. These are user-level (`systemctl --user`)
|
|
units running as the deploy user, not system-wide units — no root needed to
|
|
manage them day to day. Because user services normally stop when the user logs
|
|
out, enable lingering once so they keep running:
|
|
|
|
```sh
|
|
sudo loginctl enable-linger <deploy-user>
|
|
```
|
|
|
|
The production server runs on <http://localhost:3010>, `tssbot-backend` on
|
|
<http://127.0.0.1:6000> (see `BACKEND_PORT`), and the webhook listener on
|
|
`WEBHOOK_PORT`. Each runs as a single instance — a deploy restart is a plain
|
|
`systemctl --user restart`, so expect a brief (roughly 1-2 second) connection
|
|
drop while `tssbot-web` restarts, rather than PM2-style zero-downtime cluster
|
|
reloads.
|
|
|
|
Useful commands:
|
|
|
|
```sh
|
|
systemctl --user status tssbot-web tssbot-webhook tssbot-backend
|
|
journalctl --user -u tssbot-web -f
|
|
systemctl --user restart tssbot-web
|
|
```
|
|
|
|
The server serves `/health`
|
|
locally and only proxies the API routes used by the app:
|
|
|
|
- `GET /api/tss/leaderboard/teams?limit=1..100`
|
|
- `GET /api/tss/games/recent?limit=1..100`
|
|
- `GET /api/tss/games/:session_id?lang=...`
|
|
- `GET /api/tss/games/:session_id/logs`
|
|
- `GET /api/tss/teams/resolve?name=...`
|
|
- `GET /api/tss/teams/:team`
|
|
- `GET /api/tss/teams/:team/history`
|
|
- `GET /api/tss/teams/:team/games`
|
|
|
|
Vehicle icon PNGs are served statically at `/vehicle-icons` from `VEHICLE_ICONS_DIR`
|
|
(populated at deploy from `SHARED/ICONS/VEHICLES`).
|
|
|
|
The proxy blocks cross-origin/API-navigation requests, strips CORS headers from
|
|
the upstream response, rate limits callers, and caches successful GET responses.
|
|
Public TSS reads are written to a bounded JSON snapshot cache and served through
|
|
their normal `/api/tss/*` route. Fresh snapshots return without touching the
|
|
backend; stale snapshots are served immediately while the server refreshes them
|
|
in the background. Matching `/data/*` paths are also available for diagnostics or
|
|
static-first experiments, but the frontend uses `/api/tss/*` by default so the
|
|
site stays dynamic. All responses
|
|
ship `X-Content-Type-Options`, `X-Frame-Options: DENY`, `Referrer-Policy`,
|
|
`Permissions-Policy`, `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy`,
|
|
HSTS (over HTTPS), and HTML responses include a Content Security Policy that
|
|
allows only Cloudflare Turnstile and the CARTO basemap tiles.
|
|
|
|
Override the API target by setting it in `.env` and restarting:
|
|
|
|
```sh
|
|
echo 'API_UPSTREAM=http://127.0.0.1:8080' >> .env
|
|
systemctl --user restart tssbot-web
|
|
```
|
|
|
|
Set `PUBLIC_ORIGIN` to the public site origin in production, especially behind a
|
|
reverse proxy (same `.env` + restart pattern as above).
|
|
|
|
Optional API protection tuning:
|
|
|
|
```sh
|
|
API_CACHE_TTL_MS=15000
|
|
PUBLIC_DATA_CACHE_DIR=~/tsswebstorage/public-data
|
|
PUBLIC_DATA_CACHE_FRESH_MS=300000
|
|
PUBLIC_DATA_CACHE_STALE_MS=86400000
|
|
PUBLIC_DATA_PREWARM_INTERVAL_MS=300000
|
|
PUBLIC_DATA_COLD_TIMEOUT_MS=8000
|
|
VITE_STATIC_DATA=false
|
|
VITE_SITE_GATE=true
|
|
API_RATE_LIMIT_WINDOW_MS=60000
|
|
API_RATE_LIMIT_MAX=120
|
|
SITE_SESSION_SECRET=long-random-shared-secret
|
|
SITE_SESSION_TTL_SECONDS=43200
|
|
```
|
|
|
|
Successful Turnstile verification sets signed, HttpOnly Turnstile and site-session
|
|
cookies. `/api/*` and `/data/*` requests must present those cookies plus
|
|
same-origin browser request metadata, so the data is served to verified active
|
|
site sessions instead of as an open public API.
|
|
|
|
On startup, the web server preloads the critical public snapshots: team
|
|
leaderboard, player leaderboard, home teams, and recent games. `/health`
|
|
includes a `public_data` block with the latest preload status. A same-origin
|
|
`POST /api/cache/prewarm` refreshes those snapshots on demand.
|
|
|
|
## Reverse proxy / Cloudflare
|
|
|
|
The server only honours `CF-Connecting-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`,
|
|
and the Cloudflare geolocation headers when the immediate TCP peer is listed in
|
|
`TRUSTED_UPSTREAM_IPS`. Bind the server to `127.0.0.1` (or leave it on `0.0.0.0`
|
|
behind a firewall) and front it with nginx/Cloudflare for the configuration to
|
|
take effect:
|
|
|
|
```sh
|
|
TRUST_PROXY=cloudflare
|
|
TRUSTED_UPSTREAM_IPS=127.0.0.1,::1,::ffff:127.0.0.1
|
|
```
|
|
|
|
Set `TRUST_PROXY=none` if the server is exposed directly. Without it, an attacker
|
|
that reaches the app port can spoof client-IP headers to bypass rate limiting.
|
|
|
|
## Uptime snapshots
|
|
|
|
The production server samples uptime every 30 minutes and exposes the history at
|
|
`/api/uptime`. Snapshots are stored in SQLite under `~/tsswebstorage` by
|
|
default. Set `UPTIME_STORAGE_DIR` to choose a different folder:
|
|
|
|
```sh
|
|
UPTIME_STORAGE_DIR=~/tsswebstorage
|
|
UPTIME_DATABASE_FILE=uptime.sqlite
|
|
UPTIME_SAMPLE_INTERVAL_MS=1800000
|
|
UPTIME_HISTORY_LIMIT=336
|
|
```
|
|
|
|
The server creates the storage folder, SQLite database, and `uptime_snapshots`
|
|
table automatically.
|
|
|
|
## Viewer analytics
|
|
|
|
The site shows a centered cookie notice before analytics start. The first screen
|
|
offers `Allow all` or `Configure`; detailed settings only appear after
|
|
`Configure`. A necessary cookie remembers the visitor's choice. If a visitor
|
|
allows analytics, the browser sends page-view and heartbeat events to
|
|
`POST /api/viewers/event`. Visitors can choose whether to include browser/device,
|
|
screen, language/timezone, referrer, and technical diagnostics. Technical
|
|
diagnostics can include HTTP version, protocol headers, content negotiation
|
|
headers, browser privacy signals, network quality, touch support, CPU/memory
|
|
hints, and similar debugging fields. The public `/viewers` page reads `GET
|
|
/api/viewers` and shows active pages, 24-hour page totals, top pages, and any
|
|
consented client details.
|
|
|
|
Viewer analytics are stored in SQLite under the same `UPTIME_STORAGE_DIR` by
|
|
default. Raw IP addresses and IP hashes are not stored in viewer analytics.
|
|
Withdrawing consent removes the local visitor ID and calls
|
|
`POST /api/viewers/delete` to delete matching visitor/session analytics records.
|
|
The `/privacy` page lists the controller, contact route, purposes, retention,
|
|
rights, and complaint routes.
|
|
|
|
```sh
|
|
ANALYTICS_DATABASE_FILE=viewers.sqlite
|
|
ANALYTICS_RETENTION_DAYS=30
|
|
ANALYTICS_ACTIVE_WINDOW_SECONDS=75
|
|
```
|
|
|
|
This is an implementation aid, not legal advice. For production GDPR compliance,
|
|
keep the `/privacy` page aligned with the configured retention period, hosting
|
|
setup, and actual data fields.
|
|
|
|
## GitHub webhook
|
|
|
|
The webhook process listens on port `3011` at `/github`. Configure GitHub to send
|
|
push events there.
|
|
|
|
A webhook secret is required — without `GITHUB_WEBHOOK_SECRET`, the webhook
|
|
rejects every request. Put it in `.env` in the project root (recommended over
|
|
inlining the secret in a shell command, which writes it to shell history), then
|
|
restart:
|
|
|
|
```sh
|
|
echo 'GITHUB_WEBHOOK_SECRET=your-secret' >> .env
|
|
systemctl --user restart tssbot-webhook
|
|
```
|
|
|
|
The webhook only deploys pushes whose `ref` is in `GITHUB_WEBHOOK_REFS`
|
|
(default `refs/heads/main`). Optionally pin the repository:
|
|
|
|
```sh
|
|
GITHUB_WEBHOOK_REFS=refs/heads/main
|
|
GITHUB_WEBHOOK_REPOSITORY=owner/repo
|
|
```
|
|
|
|
Deploys run `npm ci` (not `npm install`) so an attacker who compromises a
|
|
dependency cannot quietly add new packages — the lockfile is the source of
|
|
truth.
|
|
|
|
Set `DISCORD_INCLUDE_PATCH=true` only if the Discord channel is private; by
|
|
default the patch preview is omitted from Discord notifications to avoid
|
|
leaking diff contents.
|
|
|
|
The default deploy flow is:
|
|
|
|
```sh
|
|
git pull --ff-only
|
|
npm ci --include=dev --include=optional
|
|
npm run build -- --outDir ../dist-next
|
|
cargo build --manifest-path backend/Cargo.toml --release
|
|
# the webhook promotes dist-next to dist after carrying over old hashed assets
|
|
systemctl --user restart tssbot-web tssbot-backend
|
|
```
|
|
|
|
Only services listed in `RESTART_TARGETS` are restarted. The default is
|
|
`tssbot-web,tssbot-backend`, so unrelated systemd units are left alone. The web
|
|
server handles `SIGINT` and `SIGTERM` by closing its listener and SQLite
|
|
handles before exit, giving systemd a clean shutdown within `TimeoutStopSec`
|
|
instead of a hard kill. The webhook exits cleanly every 24 hours; its unit uses
|
|
`Restart=always` so systemd relaunches it right away.
|
|
|
|
When webhook code changes are deployed, the webhook restarts itself once
|
|
(delayed so its own deploy response/notifications land first) so it loads the
|
|
updated listener:
|
|
|
|
```sh
|
|
systemctl --user restart tssbot-webhook
|
|
```
|
|
|
|
The webhook listener reads `.env` on startup. To send Discord notifications for
|
|
listener restarts and GitHub push deploy start/success/failure events, set:
|
|
|
|
```sh
|
|
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
|
|
```
|
|
|
|
Deploy completion and failure notifications include a changed-file summary and a
|
|
truncated patch preview for the pushed diff.
|