announcer/compose.yaml
Kent c4a15799dd Mesh: én sentral fordeler til flere soner
Flere anlegg – hovedscene, resepsjon, crew – og ett sted å sende fra, med
avkryssing for hvilke soner meldingen skal til.

Sentralen er en vanlig announcer med [zones] i config. En sone er en vanlig
announcer med ANNOUNCER_MESH_SECRET. Samme image, ingen mode, og en sone kan
brukes alene hvis sentralen faller ut. Hver avhengighet med C-kode har
aarch64-wheel i uv.lock, så det samme bildet går på en Raspberry Pi uten å
kompilere noe.

Sentralen syntetiserer én gang og sender ferdig WAV (POST /api/play,
multipart). Sonen trenger da verken nett, edge-tts eller Pipers 670 MB for
det den får – Item.wav er hele endringen i avspillingen. Køene forblir per
sone: dispatcheren har egen tråd og egen kø, leverer til hver sone i hver sin
tråd med timeout, og går videre. En opptatt hovedscene holder ikke igjen
resepsjonen, og en sone som er nede feiler alene – synlig per sone i UI og
revisjonslogg.

Auth gjenbrukes: ny leverandør mesh på header X-Mesh-Key (ikke Bearer – den
leses av JWT-leverandøren, som avviser alt som ikke er et JWT før neste får
prøve), med én handling, play. To regler som fulgte med:

- play følger aldri med «alt åpent». Uten det kunne hvem som helst POSTe en
  WAV til en åpen sone og få den spilt. Testen for åpen modus fanget det.
- En legitimasjon som vises fram og er feil gir 401 også der gjester ellers
  har lov. Før slapp en feil PIN i åpen modus stille gjennom som gjest.

mesh teller ikke som innlogging for folk: en sone med bare nøkkelen har
fortsatt åpen side lokalt.

Planlagte sendinger husker sonene sine og fordeles gjennom dispatcheren;
planen ender som sent hvis minst én sone tok imot, med «hovedscene: kø #3,
crew: nede» som note.

Hodeløs Pi: install.sh kjører loginctl enable-linger, ellers finnes ikke
lydsocketen når ingen logger inn. docs/mesh.md tar også raspi-config for
jack framfor HDMI.

38 nye tester, 148 totalt. Fordelingen testes uten nett med stubbet
transport; selve HTTP-en testes mot en levende werkzeug-server, så den
håndrullede multiparten møter Flask sin parser. Verifisert i compose med
sentral og to soner: til alle, bare til én, og én sone stoppet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 02:22:05 +02:00

58 lines
2.4 KiB
YAML

# Announcer i drift på klienten ved anlegget.
#
# ./install.sh setter opp alt, inkludert .env
# docker compose up -d senere, for hånd
#
# Testoppsettet med dummy-IdP ligger i compose.test.yaml, ikke her.
name: announcer
services:
announcer:
build: .
image: announcer:local
restart: unless-stopped
# BIND_IP er sikkerhetsmekanismen her, ikke ufw. Dockers publiserte
# porter går forbi ufw, så en ufw-regel ville ikke stengt noen ute.
# install.sh setter den til NetBird-adressen når wt0 finnes.
ports:
- "${BIND_IP:-0.0.0.0}:${ANNOUNCER_PORT:-8090}:8090"
# Må være brukeren som eier lydsocketen på verten, ellers kommer det
# ingen lyd. install.sh fyller inn id -u / id -g.
user: "${PUID:-1000}:${PGID:-1000}"
environment:
ANNOUNCER_SINK: ${ANNOUNCER_SINK:-}
ANNOUNCER_TITLE: ${ANNOUNCER_TITLE:-}
ANNOUNCER_PUBLIC_URL: ${ANNOUNCER_PUBLIC_URL:-}
# Hemmeligheter: hører i .env, ikke i config.toml.
ANNOUNCER_PIN: ${ANNOUNCER_PIN:-}
ANNOUNCER_JWT_SECRET: ${ANNOUNCER_JWT_SECRET:-}
ANNOUNCER_OIDC_CLIENT_SECRET: ${ANNOUNCER_OIDC_CLIENT_SECRET:-}
ANNOUNCER_SECRET_KEY: ${ANNOUNCER_SECRET_KEY:-}
ANNOUNCER_MESH_SECRET: ${ANNOUNCER_MESH_SECRET:-}
XDG_RUNTIME_DIR: ${XDG_RUNTIME_DIR:-/run/user/1000}
# Planlagte sendinger lagres som absolutt tid, så TZ påvirker dem ikke.
# Men uten den står loggen i UTC mens du står i CEST, og da blir det
# vanskelig å se om noe gikk som det skulle.
TZ: ${TZ:-Europe/Oslo}
volumes:
# Stemmer, buffer, logg, sesjonsnøkkel, egne jingler og config.toml.
- ./data:/data
# Lyden. Hele XDG_RUNTIME_DIR, ikke bare én socket, så både
# PipeWire (pipewire-0) og PulseAudio (pulse/native + cookie) virker.
- ${XDG_RUNTIME_DIR:-/run/user/1000}:${XDG_RUNTIME_DIR:-/run/user/1000}
# Containeren starter ved boot, altså før noen er logget inn grafisk.
# Da finnes ikke lydsocketen ennå, og avspilling feiler. Det retter seg
# selv: player_cmd() slås opp per melding, så første utrop etter
# innlogging virker uten at noe må startes om.
healthcheck:
test: ["CMD", "python", "-c",
"import urllib.request;urllib.request.urlopen('http://127.0.0.1:8090/api/config',timeout=5)"]
interval: 30s
timeout: 10s
start_period: 180s # første oppstart laster ned ~670 MB stemmer
retries: 3