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>
This commit is contained in:
parent
eb27a162d5
commit
c4a15799dd
17 changed files with 1486 additions and 23 deletions
57
AGENTS.md
57
AGENTS.md
|
|
@ -19,10 +19,12 @@ Filer:
|
|||
| `auth.py` | `Principal`, leverandørene, OIDC-rutene |
|
||||
| `config.py` | `config.toml` + miljøvariabler, standardverdier |
|
||||
| `schedule.py` | planlagte sendinger, lagret i `data/scheduled.json` |
|
||||
| `mesh.py` | sentral + soner: fordeling, transport, statuspolling |
|
||||
| `index.html` | hele sida, inline CSS/JS, ingen byggesteg |
|
||||
| `config.example.toml` | kommentert mal; kopier til `config.toml` |
|
||||
| `docs/crew-integration.md` | kontrakten mot crew-panelet |
|
||||
| `tests/` | `uv run pytest` – 110 tester: auth, planlegging og HTTP-laget |
|
||||
| `docs/mesh.md` | flere anlegg: oppsett, bruk, feiltilfeller, protokoll |
|
||||
| `tests/` | `uv run pytest` – 148 tester: auth, planlegging, mesh og HTTP-laget |
|
||||
| `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 |
|
||||
|
|
@ -203,6 +205,51 @@ veien for å prøve en endring kjapt; `uv run pytest` for testene. Men
|
|||
det er ryddet bort. Skal noe prøves med innlogging, bruk
|
||||
`compose.test.yaml`.
|
||||
|
||||
## Mesh: én sentral, flere soner
|
||||
|
||||
Brukerdokumentasjonen står i `docs/mesh.md`. Her er det som gjelder koden.
|
||||
|
||||
`mesh.py` kjenner verken Flask eller `Player`. `Dispatcher` får
|
||||
`synthesize`, `play_local` og `send` inn som funksjoner, og det er det som
|
||||
gjør at hele fordelingen testes uten nett i `tests/test_mesh.py`. Den ekte
|
||||
transporten (`send_to_zone` sin håndrullede multipart mot Flask sin parser)
|
||||
prøves i `test_routes.py` mot en levende werkzeug-server – det er den
|
||||
testen som fanger en feil i selve HTTP-en.
|
||||
|
||||
- **Sentral eller sone avgjøres av config, ikke kode.** `[zones]` satt →
|
||||
`DISPATCHER` og `POLLER` finnes, `/api/announce` tar `zones`. Ellers er
|
||||
begge `None` og alt går i lokal kø nøyaktig som før. `ANNOUNCER_MESH_SECRET`
|
||||
satt → `MeshProvider` er aktiv og `/api/play` tar imot. Begge kan være
|
||||
satt på samme maskin (sentral med eget anlegg).
|
||||
- **Egen tråd og egen kø for fordeling**, atskilt fra `Player`. Hadde
|
||||
fordelingen gått gjennom sentralens avspillingskø, ville en melding til
|
||||
resepsjonen ventet på at hovedscenen ble ferdig. Dispatcheren
|
||||
syntetiserer, leverer til hver sone i hver sin tråd (timeout per sone), og
|
||||
er ferdig. Hver sone køer selv.
|
||||
- **`Item.wav`** er hele endringen i avspillingen: `run_once()` bruker
|
||||
`item.wav or synthesize(...)`. En sone syntetiserer aldri det den får fra
|
||||
sentralen.
|
||||
- **`play` følger aldri med «alt åpent».** `Authenticator.anonymous()`
|
||||
trekker den fra. Uten det kunne hvem som helst POSTe en WAV til en åpen
|
||||
sone og få den spilt. Og en legitimasjon som vises fram og er feil (PIN,
|
||||
token, mesh-nøkkel) gir nå 401 også der gjester ellers har lov –
|
||||
`require()` sjekker `error()` først. Det var ikke slik før; en feil PIN
|
||||
i åpen modus slapp stille gjennom som gjest.
|
||||
- **`MeshProvider` teller ikke som innlogging for folk.** `Authenticator`
|
||||
skiller `providers` fra `human`; `enabled` og `/api/config`-blokka ser
|
||||
bare på de menneskelige. Ellers ville en sone med bare mesh-nøkkel krevd
|
||||
innlogging lokalt uten noen måte å logge inn på.
|
||||
- **Planlagt sending i mesh**: `Scheduled.zones` lagres med planen.
|
||||
`scheduler_tick()` fordeler via `DISPATCHER` når den finnes, og planen
|
||||
ender som `sent` hvis minst én sone tok imot (`_dispatch_finished`), med
|
||||
`summary()` som note – `hovedscene: kø #3, crew: nede`. Det lokale
|
||||
køelementet bærer *ikke* `schedule_id` i mesh-modus; fordelingen eier
|
||||
planen. Nevnes fordi det er motsatt av lokal modus.
|
||||
- **Statuspolling er én tråd** (`ZonePoller`), cachet, så N nettlesere ikke
|
||||
gir N × soner kall. Lokal sone slås opp ferskt per snapshot.
|
||||
- **Rekkefølgen i `zones`** i en forespørsel bevares i `Dispatch.zones`,
|
||||
og dermed i `summary()` og i UI-et. Testene antar det.
|
||||
|
||||
## Planlagte sendinger
|
||||
|
||||
`schedule.py` er den første delen av appen som **må** ligge på disk. Kø og
|
||||
|
|
@ -315,6 +362,14 @@ Vil Kent ha enda bedre lyd: ElevenLabs (nøkkel, best norsk) eller OpenAI
|
|||
- **Én prosess.** `main()` kjører waitress med mange tråder, ikke gunicorn
|
||||
med flere workere: `Player` og planleggeren er singletons i prosessen, og
|
||||
flere workere ville gitt flere køer som snakker i munnen på hverandre.
|
||||
- **Mesh-nøkkelen må være lik** på sentral og sone, ellers `✗ avvist` med
|
||||
`401 Feil mesh-nøkkel` på fordelingen. Og sonens `BIND_IP` må gjøre den
|
||||
nåbar fra sentralen – med NetBird er det NetBird-adressen som skal stå i
|
||||
`[zones]`.
|
||||
- **En sone uten lydsocket** (hodeløs Pi uten linger, eller feil
|
||||
`PUID`) tar imot og køer, men hvert utrop ender `failed`. Det ser riktig
|
||||
ut fra sentralen (`✓` = tatt imot) helt til man ser på sonens egen kø.
|
||||
`install.sh` slår på `loginctl enable-linger` nettopp derfor.
|
||||
- **Oppgradering:** `schedule` er en ny handling. Har et oppsett et eksplisitt
|
||||
`[roles]` i `config.toml`, får `crew` den *ikke* automatisk – eksplisitt
|
||||
konfigurasjon overstyrer standardverdiene, og da blir svaret 403 «Rollen din
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue