2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00
2026-05-20 23:51:18 +02:00

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

cp .env.example .env
# edit .env: set HA_TOKEN and PUBLIC_BASE_URL
docker compose up -d --build

Then open http://<host>:8091.

Run locally (no Docker)

Requires Node.js 20+.

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.

Description
D&D effects soundboard with restricted Home Assistant playback backend
Readme 148 KiB
Languages
JavaScript 73.8%
CSS 12.3%
Shell 8.4%
HTML 5%
Dockerfile 0.5%