Docker er driftsformen, ikke bare en testrigg

Announcer skal kjøre standalone på klienten som er koblet til anlegget, i
Docker – aldri på utviklingsmaskina. install.sh installerte nativt med apt
og uv, og det passer ikke lenger.

- install.sh er nå en Docker-bootstrap: installerer Docker om den mangler,
  lager .env med riktig bruker-ID og portbinding, oppretter data/, bygger,
  starter, og setter ufw. Den rydder også bort gamle native enheter, både
  announcer.service og sg-announcer.service.
- Dockerfile kopierer koden inn istedenfor å låne den fra et bind-mount, så
  det som kjører i drift er uavhengig av hva som ligger i mappa.
- Alt som skal overleve en ny container ligger i data/ på verten. Bind-mount
  og ikke navngitt volum, med vilje: da kan man lese
  data/logs/announcements.jsonl og slippe egne jingler i data/chimes/ uten å
  gå inn i containeren, og eierskapet følger brukeren istedenfor root.
- Testriggen er flyttet til compose.test.yaml, med eget prosjektnavn, så den
  ikke kan forveksles med eller forstyrre drift.

Lyden ut av containeren krever tre ting, og de er dokumentert i AGENTS.md:
hele XDG_RUNTIME_DIR montert inn (ikke bare pipewire-socketen – PulseAudio
trenger pulse/native og en cookie i samme mappe, og da virker begge
lydservere), user: PUID:PGID lik eieren av socketen, og en innlogget grafisk
økt. Containeren starter ved boot, altså før innlogging, så avspilling feiler
til noen logger inn – det retter seg selv, siden player_cmd() slås opp per
melding. Verifisert: lyden spiller fra driftscontaineren.

Brannmuren er den viktige endringen fra nativ tid. Dockers publiserte porter
settes opp i nat-tabellen, før ufws regler, så en ufw-regel på porten gjør
ingenting. Tilgangen styres nå av BIND_IP i .env, og install.sh setter den til
NetBird-adressen når wt0 finnes. ufw settes fortsatt for ssh, men er ikke
involvert i tilgangen til announcer.

Testriggen bindes nå til 127.0.0.1. Den sto med PIN 1234 publisert på
0.0.0.0, og gikk altså forbi ufw ut på LAN-et.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kent 2026-10-08 16:07:59 +02:00
parent 6aaa98d8b1
commit 6c1253f54c
7 changed files with 372 additions and 177 deletions

122
AGENTS.md
View file

@ -22,45 +22,98 @@ Filer:
| `config.example.toml` | kommentert mal; kopier til `config.toml` |
| `docs/crew-integration.md` | kontrakten mot crew-panelet |
| `tests/test_auth.py` | `uv run pytest` |
| `compose.yaml` + `Dockerfile` | testoppsett, se under. Ikke produksjon |
| `Dockerfile` + `compose.yaml` | **drift** – appen kjøres i Docker |
| `.env.example` | bruker-ID, portbinding og hemmeligheter |
| `compose.test.yaml` | testoppsett med dummy-IdP, se under |
| `docker/dummy_idp.py` | falsk crew-panel, så OIDC kan prøves |
## Kjøring
Tjenesten er en systemd-brukerenhet: `~/.config/systemd/user/announcer.service`
(enabled, port 8090). `systemctl --user restart announcer` etter endringer i
koden. Manuelt: `uv run announcer.py --port 8090`.
**Announcer kjører i Docker, på en egen klient som er koblet til
høyttaleranlegget – aldri på kitsune.** Kitsune er utviklingsmaskin; der er
det bare koden som bor.
Ny maskin (Debian/Ubuntu/Mint): `git clone https://git.1x1.no/kent/announcer.git`
og `./install.sh` gjør alt (apt, uv, Python 3.13, stemmer, enhet, ufw). Finnes
NetBird (`wt0`) skrur det på ufw med deny incoming og åpner 22, 22022 og porten
kun på `wt0`. Kan kjøres på nytt, og rydder da bort en gammel
`sg-announcer.service` hvis den ligger der. Avspilling bruker `pw-play` når
PipeWire kjører, ellers `paplay` (`player_cmd()`), så Mint 21 med PulseAudio
virker også.
Ny klient (Debian/Ubuntu/Mint):
### Omdøpingen: ett punkt gjenstår
```bash
git clone https://git.1x1.no/kent/announcer.git
cd announcer && ./install.sh
```
Mappa er flyttet til `~/Work/announcer`, enheten heter `announcer.service`, og
remote peker på `https://git.1x1.no/kent/announcer.git`. **Men repoet i Forgejo
heter `annuoncer`** – en skrivefeil. Rett navnet i Forgejo, så virker remote;
Forgejo legger selv inn en omdirigering fra det gamle navnet.
`install.sh` installerer Docker om den mangler, lager `.env` med riktig
bruker-ID og portbinding, oppretter `data/`, bygger bildet, starter
containeren og setter ufw. Den rydder også bort en eventuell gammel nativ
`announcer.service`/`sg-announcer.service`. Kan kjøres på nytt; en `.env` som
finnes blir ikke overskrevet.
Flytter du mappa igjen: `.venv` har absolutte stier i `pyvenv.cfg` og i
shebangen til hvert konsollskript, så `uv sync` alene er ikke nok. Da må
`rm -rf .venv && uv sync` til, og tjenesten startes om.
Drift:
```bash
docker compose logs -f # følg loggen
docker compose restart # etter endringer i data/config.toml
docker compose up -d --build # etter endringer i koden
cat data/logs/announcements.jsonl # hvem som sendte hva
```
Alt som skal overleve en ny container ligger i `data/` på verten
(bind-mount, ikke navngitt volum, så du kan lese loggen og slippe egne
jingler i `data/chimes/` uten å gå inn i containeren):
| | |
|---|---|
| `data/voices/` | Piper-modellene, ~670 MB, lastes ned ved første oppstart |
| `data/cache/` | ferdige WAV-er etter innholdshash. Trygt å slette |
| `data/chimes/` | egne jinglefiler |
| `data/logs/` | revisjonsloggen |
| `data/instance/` | sesjonsnøkkelen |
| `data/config.toml` | valgfri. Uten den er sida åpen |
### Lyden ut av containeren
Det eneste virkelig uvanlige i oppsettet. Tre ting må stemme:
1. Hele vertens `XDG_RUNTIME_DIR` monteres inn på samme sti. Ikke bare
`pipewire-0`: PulseAudio bruker `pulse/native` pluss en cookie i samme
mappe, og ved å ta hele mappa virker begge lydservere uten at compose må
vite hvilken maskina har. `player_cmd()` velger `pw-play` eller `paplay`.
2. `user: "${PUID}:${PGID}"` må være brukeren som **eier** socketen på
verten. Stemmer ikke ID-en, kommer det ingen lyd. `install.sh` fyller inn
`id -u`/`id -g`.
3. Maskina må være logget inn grafisk, ellers finnes socketen ikke.
Containeren starter ved boot, altså før innlogging, og avspilling feiler
til noen logger inn. Det retter seg selv: `player_cmd()` slås opp per
melding, så første utrop etter innlogging virker uten omstart. Derfor
anbefaler `install.sh` automatisk innlogging.
### Brannmur: BIND_IP, ikke ufw
**Dockers publiserte porter går forbi ufw.** De settes opp i nat-tabellen,
før ufws regler, så `ufw deny` på porten gjør ingenting. Tilgangen styres
derfor av `BIND_IP` i `.env`:
| | |
|---|---|
| `0.0.0.0` | alle nett maskina er på |
| `100.75.x.x` | bare NetBird. `install.sh` velger dette når `wt0` finnes |
| `127.0.0.1` | bare maskina selv |
ufw settes fortsatt for maskinens øvrige porter (ssh på `wt0`), men den er
ikke involvert i tilgangen til announcer. Dette er en endring fra den native
tida, da porten måtte åpnes eksplisitt i ufw.
## Testing i Docker
`docker compose up --build` gir to containere:
`docker compose -f compose.test.yaml up --build` gir to containere:
| | |
|---|---|
| `http://localhost:8091` | announcer med PIN, JWT **og** OIDC slått på (`docker/config.test.toml`) |
| `http://localhost:9000` | `docker/dummy_idp.py`, som later som den er crew-panelet |
Port 8091 med vilje, så den ikke kolliderer med tjenesten på 8090.
Egen fil, eget prosjektnavn og port 8091, så den ikke kan forveksles med
eller forstyrre den som kjører i drift. Begge porter er bundet til
`127.0.0.1`, så testriggen – som har PIN `1234` – aldri ligger åpen på
LAN-et.
Dummy-IdP-en finnes fordi crew-panelet ikke har OIDC ennå: uten den kan
OIDC-veien ikke prøves i det hele tatt. Den kjører en ekte
@ -80,9 +133,11 @@ Lyd: `/run/user/1000` er montert inn, så `pw-play` i containeren spiller på
vertens anlegg. Kommenter ut den linja i `compose.yaml` hvis du bare skal
teste innlogging.
`UV_PROJECT_ENVIRONMENT=/opt/venv` i bildet er ikke pynt: `/app` er et
bind-mount av repoet, og uten det ville containerens `uv sync` overskrevet
`.venv` som den kjørende tjenesten bruker.
`compose.test.yaml` bind-monterer koden, så endringer slår inn med en
restart. Driftsbildet kopierer koden inn i stedet, så det som kjører er
uavhengig av hva som ligger i mappa. `UV_PROJECT_ENVIRONMENT=/opt/venv` er
derfor ikke pynt: med bind-mount ville containerens `uv sync` ellers
overskrevet `.venv` på verten.
## Konfigurasjon
@ -138,6 +193,14 @@ Uten den logges alle ut ved hver `systemctl restart`. `SESSION_COOKIE_SECURE`
settes bare når `server.behind_tls` er sann – appen kjører på rå HTTP over LAN
og NetBird, og med Secure på ville cookien aldri blitt sendt.
## Utvikling på kitsune
`uv run announcer.py --port 8099` kjører appen direkte, uten Docker. Det er
veien for å prøve en endring kjapt; `uv run pytest` for testene. Men
**ingen tjeneste skal stå og kjøre på kitsune** – det var slik det var før, og
det er ryddet bort. Skal noe prøves med innlogging, bruk
`compose.test.yaml`.
## Hvordan det henger sammen
- `/api/config` er alt sida trenger for å tegne seg selv: branding, stemmer,
@ -187,10 +250,15 @@ Vil Kent ha enda bedre lyd: ElevenLabs (nøkkel, best norsk) eller OpenAI
## Feller
- **ufw**: appen kjører nativt, så Docker-smutthullet gjelder ikke. Porten må
åpnes eksplisitt for LAN-et (`sudo ufw allow from 10.20.70.0/23 to any port 8090 proto tcp`).
- **ufw styrer ikke Docker-porter.** Se «Brannmur» over: tilgangen avgrenses
med `BIND_IP` i `.env`. En `ufw deny` på porten gir falsk trygghet.
- **Ingen lyd?** Nesten alltid `PUID`/`PGID` i `.env` som ikke er brukeren
som eier lydsocketen, eller at maskina ikke er logget inn grafisk. Sjekk
hva containeren velger:
`docker compose exec announcer python -c "import announcer,pathlib;print(announcer.player_cmd(pathlib.Path('/tmp/x.wav')))"`
- `pkill -f announcer.py` fra et skall som selv har `announcer.py` i
kommandolinja dreper skallet. Bruk `systemctl --user`.
kommandolinja dreper skallet. Gjelder utvikling med `uv run`; i drift er
det `docker compose`.
- `player = Player()` står på modulnivå, så avspillingstråden starter ved
`import announcer` – også i `install.sh`, som importerer modulen for å laste
ned stemmer. Harmløst, men greit å vite.