All checks were successful
Publish container image / Build and push image (push) Successful in 2s
190 lines
6.9 KiB
Markdown
190 lines
6.9 KiB
Markdown
# 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://<lan-ip>: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://<docker-host-lan-ip>: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://<host>:8091`.
|
|
|
|
## Deployment helper for published images
|
|
|
|
For host deployments on this machine, use `deploy/deploy.sh` instead of editing
|
|
compose files by hand. The script is meant to be copied into other repos too: it
|
|
infers the repo name from `origin`, looks up the numeric Gitea repo id via the
|
|
API, computes the host port as `5000 + 10*repo_id + service_number`, copies the
|
|
repo's deployment assets, and writes a ready-to-run deployment directory.
|
|
|
|
For Arcana FX the Gitea repo id is `4`, so the default deployment port is:
|
|
|
|
```text
|
|
5000 + 10*4 + 0 = 5040
|
|
```
|
|
|
|
Generate a deployment directory for a tagged image:
|
|
|
|
```bash
|
|
./deploy/deploy.sh v0.1.0
|
|
```
|
|
|
|
That creates `~/deployments/arcana-fx-0/` containing:
|
|
|
|
- `docker-compose.yml` pinned to `192.168.33.22:3300/gemma/arcana-fx:v0.1.0`
|
|
- `.env` copied from `.env.example` on first run
|
|
- `config/` copied from the repo via `deploy/include-paths.txt`
|
|
- `.deploy.meta` with the resolved repo id, version, and host port
|
|
|
|
Then edit the generated `.env` and start it:
|
|
|
|
```bash
|
|
cd ~/deployments/arcana-fx-0
|
|
$EDITOR .env
|
|
docker compose pull
|
|
docker compose up -d
|
|
```
|
|
|
|
A second service instance can use another service number, which shifts the port
|
|
by 1 while keeping the same repo-id block:
|
|
|
|
```bash
|
|
./deploy/deploy.sh v0.1.0 1
|
|
# host port becomes 5041
|
|
```
|
|
|
|
When copying this deployment helper to another repo, update
|
|
`deploy/docker-compose.template.yml` for that app's internal port and mounts,
|
|
and `deploy/include-paths.txt` for any extra files/directories that must travel
|
|
with the generated compose file.
|
|
|
|
## 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/<id>/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=<absolute>&share=<id>` to walk directories.
|
|
3. `GET /api/files/cat?path=<absolute>&share=<id>` 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.
|