# Arcana FX A dark-fantasy soundboard for D&D nights. The frontend shows a tile grid of audio effects; pressing a tile asks the backend to play that file through a configured Home Assistant `media_player`. Future panels for light control are present but disabled. ## Architecture ``` ┌────────────┐ /api/play ┌──────────────────────┐ POST media_player/play_media │ Frontend │ ─────────────▶│ Arcana FX backend │ ───────────────────────────────▶ Home Assistant │ (static) │ │ (Node 20 + Express) │ │ └────────────┘ │ │◀──── fetches /media ─────────────────┘ │ Filestash client │ ◀── share proof cookie + cat ── Filestash └──────────────────────┘ ``` - **Backend** (`src/`) is the only component that holds the Home Assistant token and the Filestash proof cookie. It exposes a deliberately narrow API. - **Frontend** (`public/`) is plain static HTML/CSS/JS — no build step. - Home Assistant pulls audio from `PUBLIC_BASE_URL/media?path=...`, which the backend streams from Filestash on demand. Nothing is preloaded. ## API surface | Method | Path | Description | |-------:|------------------|------------------------------------------------------------| | GET | `/health` | Liveness probe + cache stats | | GET | `/api/config` | Public config (media player entity, light-control stubs) | | GET | `/api/effects` | Recursive list of audio effects (`?refresh=1` to force) | | POST | `/api/play` | Body `{ "path": "/Combat/Sword.mp3" }` | | POST | `/api/stop` | Stops the configured media player | | GET | `/media?path=…` | Intranet proxy streaming the file from Filestash | `path` must be a share-relative path that was returned by `/api/effects`. Arbitrary Filestash paths, HA entities, or HA services cannot be reached through this API. ## Configuration Two layers: - `config/config.json` — non-secret, version-controlled. Filestash share, allowed extensions, allowed media player entity, cache TTL, light-control placeholders. - `.env` — secrets and per-deployment values. Copy from `.env.example`: ``` HA_URL=http://192.168.33.33:8123 HA_TOKEN=... # long-lived access token, never commit PUBLIC_BASE_URL=http://:8091 # URL HA uses to fetch /media PORT=8091 ALLOWED_ORIGINS= # optional CORS allow-list ``` `PUBLIC_BASE_URL` must be reachable **from Home Assistant**, not just from your browser. On a typical home LAN it is `http://:8091`. The allowed media player entity is `media_player.audio_test_media_player`, hardcoded in `config/config.json`. Requests for any other entity are rejected with `403`. ## Run with Docker Compose ```bash cp .env.example .env # edit .env: set HA_TOKEN and PUBLIC_BASE_URL docker compose up -d --build ``` Then open `http://:8091`. ## Container publishing pipeline A Gitea Actions workflow lives at `.gitea/workflows/release-container.yml`. Whenever a git tag is pushed, the self-hosted runner: 1. checks out the tagged commit, 2. builds the Docker image from `Dockerfile`, 3. tags it as both the sanitized git tag and `latest`, 4. pushes both tags to the Gitea container registry at `192.168.33.22:3300/gemma/arcana-fx`. The workflow authenticates with the repository Actions secret `PACKAGES_TOKEN`. Example release flow: ```bash git tag v0.1.0 git push origin v0.1.0 ``` After the workflow finishes, the image can be pulled with: ```bash docker pull 192.168.33.22:3300/gemma/arcana-fx:v0.1.0 docker pull 192.168.33.22:3300/gemma/arcana-fx:latest ``` ## Run locally (no Docker) Requires Node.js 20+. ```bash cp .env.example .env # edit .env npm install npm run check # syntax check on backend sources npm start ``` ## Filestash flow The backend obtains a proof cookie on first request: 1. `POST /api/share//proof` with `X-Requested-With: XmlHttpRequest`. Response sets the proof cookie and tells us the share's base path (e.g. `/DnD/Audio/`). 2. `GET /api/files/ls?path=&share=` to walk directories. 3. `GET /api/files/cat?path=&share=` to stream a file. Recursive listing is cached in memory (`cacheTtlSeconds` in `config/config.json`, default 10 minutes). `/api/effects?refresh=1` forces a rescan. ## Security notes - The HA token lives only in the backend process environment. It is never sent to the frontend. - Only the configured `media_player` entity is allowed. The HA client refuses any other entity id at the client layer. - Effect paths must match a known effect from the cached share scan, must start with `/`, and must not contain `..` or NUL bytes. - `.env` is git-ignored; only `.env.example` is committed. ## Future work The "Light Control" panel renders disabled placeholders (`config/config.json > lightControl.placeholders`). Wiring real switches/lights will be a new narrow set of endpoints, on the same principle as the audio side.