aboutsummaryrefslogtreecommitdiffstats
path: root/AGENTS.md
diff options
context:
space:
mode:
Diffstat (limited to 'AGENTS.md')
-rw-r--r--AGENTS.md61
1 files changed, 61 insertions, 0 deletions
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..f076d2c
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,61 @@
+# AGENTS.md
+
+A web status page for a TeamSpeak 6 server (Flask). It also lets people **join voice from the browser** through a Node.js gateway. For user-facing docs, see `README.md`.
+
+## Architecture
+
+```
+browser ──HTTP/WS :5000──▶ gateway (Node, gateway/src) ──proxy──▶ dashboard (Flask, app.py on 127.0.0.1:5001)
+ └──UDP──▶ TeamSpeak server :9987
+dashboard ──HTTPS WebQuery :8443──▶ TeamSpeak server
+```
+
+- **`run.py`** is the main entry point. It starts Flask and the gateway as child processes, and if either one exits it stops both. When voice is disabled, Node is missing, or Node is older than 22.12, only Flask runs, and it binds the public port directly. On first run it installs the gateway's npm deps automatically.
+- **`app.py`** is the Flask dashboard. It fetches channels and clients over TeamSpeak WebQuery, caches them in `cache.json`, and renders inline Jinja templates (`FRAGMENT_TEMPLATE` and `PAGE_TEMPLATE` via `render_template_string`; there is no `templates/` dir). `/?ajax=1` returns only the fragment, which the page uses for periodic refresh. A catch-all route serves images and CSS from the repo root, filtered by `ALLOWED_STATIC_EXTENSIONS` and `PRIVATE_DIRS`.
+- **`config.py`** loads `config.toml` and `.env` into `CONFIG`, and computes `VOICE_AVAILABLE`, which is true only when `[voice] enabled` is set and `TS_SERVER_PASSWORD` is non-empty.
+- **`gateway/`** is the Node voice gateway (ESM, Node ≥ 22.12):
+ - `index.js`: entry point and shutdown handling
+ - `server.js`: the public HTTP server. It handles `/ws/voice` (WebSocket), `/healthz`, and `/voice-gateway/status`, and reverse-proxies everything else to Flask.
+ - `auth.js`: `LoginLimiter` (locks out an IP after too many wrong passwords) and the Origin check
+ - `voice-session.js`: one `@honeybbq/teamspeak` client per browser, plus the JSON message protocol (`join`, `move`, `mute`, `deafen`, `chat`, `whisper`, `leave`, and replies such as `joined`, `state`, `chatMessage`, `error`, `disconnected`)
+ - `opus.js`: Opus encode/decode. It uses `@discordjs/opus` if installed and falls back to `opusscript` (WASM).
+ - `config.js`: reads the same `config.toml` and `.env` as `config.py`
+- **`static/voice.js`**: the browser voice client (UI, WebSocket, WebCodecs Opus decode, voice activity/push-to-talk, RNNoise). **`static/mic-worklet.js`**: an AudioWorklet that captures 20 ms PCM frames. **`index.css`**: all dashboard styles.
+
+## Commands
+
+```bash
+uv sync # Python deps (Python 3.13+, managed with uv)
+uv run run.py # dashboard + voice gateway → http://localhost:5000
+uv run app.py # dashboard only, no voice
+cd gateway && npm install # gateway deps (run.py does this automatically)
+cd gateway && npm test # node --test "test/*.test.js"
+docker compose up --build # container (runs `python run.py`)
+```
+
+Note: `gateway/test/` doesn't exist yet. `npm test` expects tests at `gateway/test/*.test.js` using the built-in `node:test` runner. There's no Python test suite and no linter/formatter config.
+
+## Configuration rules
+
+- **Settings** go in `config.toml`, which both Flask and the gateway read. **Secrets** go in `.env` (`TS_HOST`, `TS_API_KEY`, `TS_SERVER_PASSWORD`); see `.env.template`. Never commit `.env`.
+- Env vars override TOML values. The override table exists twice and **must stay in sync**: `ENV_OVERRIDES` in `config.py` and `ENV_OVERRIDES` in `gateway/src/config.js`. When you add a setting, add its default in both loaders (`_build_config()` / `loadConfig()`) and document it with a comment in `config.toml`.
+- Image paths in `config.toml` can be absolute URLs or paths relative to the repo root.
+
+## Security invariants (do not break)
+
+- Keep `TS_SERVER_PASSWORD` and `TS_API_KEY` out of `CONFIG`, the templates, and `VOICE_PUBLIC_CONFIG` (in `app.py`). Anything in `VOICE_PUBLIC_CONFIG` is sent to the browser.
+- The gateway must check the server password (constant-time compare, with per-IP lockout) **before** it opens any TeamSpeak connection. Cross-origin WebSocket upgrades must stay rejected unless the origin is listed in `[voice] allowed_origins`.
+- Only trust `X-Forwarded-For` when `trust_proxy = true`.
+- Enforce session limits: `max_sessions` and `max_sessions_per_ip`.
+- The static catch-all must never serve files from `PRIVATE_DIRS` or files with extensions outside the allowlist (no `.py`, `.toml`, `.env`, `.json`).
+- Pass user-supplied URLs through `safe_url()` before rendering them.
+
+## Conventions
+
+- Python: standard library first, then `requests`/Flask. Keep it simple and use module-level functions. Templates live inline in `app.py`.
+- JS: ES modules and plain Node APIs. No build step and no frontend framework. `static/*.js` is served as-is.
+- Keep the gateway pure JS/WASM so it installs on arm64 single-board computers without a compiler. Native addons must stay optional.
+- When you change `static/voice.js`, bump the cache-busting query string on its `<script>` tag in `PAGE_TEMPLATE` (currently `?v=rnnoise-2`).
+- Dashboard behaviour: spacer channels (`[Cspacer]`) are hidden. A channel whose name contains `AFK` is shown, and its client count is used as the AFK indicator. `afk_status_max_clients` limits how many per-client `clientinfo` calls are made.
+- When you add new runtime files, update the `COPY` lines in the `Dockerfile`.
+- `cache.json` and `__pycache__/` are tracked by git even though they're gitignored. Don't commit changes to them.
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage