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
25
config.py
25
config.py
|
|
@ -26,7 +26,8 @@ from typing import Any, Callable
|
|||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
# Alt man kan få lov til å gjøre. Roller er navngitte knipper av disse.
|
||||
ACTIONS = ("view", "preview", "announce", "schedule", "stop", "clear")
|
||||
# «play» er bare for mesh: en sentral som leverer en ferdig WAV til en sone.
|
||||
ACTIONS = ("view", "preview", "announce", "schedule", "stop", "clear", "play")
|
||||
|
||||
DEFAULTS: dict[str, Any] = {
|
||||
"app": {
|
||||
|
|
@ -72,6 +73,15 @@ DEFAULTS: dict[str, Any] = {
|
|||
# Hvor ofte vi ser etter forfalte sendinger.
|
||||
"tick_seconds": 20,
|
||||
},
|
||||
# Mesh: én sentral som fordeler til flere soner. [zones] settes bare på
|
||||
# sentralen (navn -> URL, eller "local" for eget anlegg); hemmeligheten
|
||||
# settes på sentralen OG hver sone. Begge tomme = ingen mesh, alt som før.
|
||||
"zones": {},
|
||||
"mesh": {
|
||||
"secret": "",
|
||||
"timeout_seconds": 5,
|
||||
"poll_seconds": 2,
|
||||
},
|
||||
"auth": {
|
||||
# Tom liste = alt åpent. Se active_providers(): en leverandør er også
|
||||
# aktiv når den er ferdig konfigurert, så det holder å sette
|
||||
|
|
@ -117,6 +127,8 @@ DEFAULTS: dict[str, Any] = {
|
|||
"anonymous": [],
|
||||
"crew": ["view", "preview", "announce", "schedule"],
|
||||
"admin": ["*"],
|
||||
# Sentralen når den snakker med en sone. view er for statuspollingen.
|
||||
"mesh": ["play", "view"],
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -150,6 +162,7 @@ ENV: dict[str, tuple[tuple[str, str], Callable[[str], Any]]] = {
|
|||
"ANNOUNCER_SECRET_KEY": (("server", "secret_key"), str),
|
||||
"ANNOUNCER_PUBLIC_URL": (("server", "public_url"), str),
|
||||
"ANNOUNCER_AUTH": (("auth", "providers"), _list),
|
||||
"ANNOUNCER_MESH_SECRET": (("mesh", "secret"), str),
|
||||
"ANNOUNCER_PIN": (("auth.pin", "pin"), str),
|
||||
"ANNOUNCER_PIN_ROLE": (("auth.pin", "role"), str),
|
||||
"ANNOUNCER_JWT_ISSUER": (("auth.jwt", "issuer"), str),
|
||||
|
|
@ -232,7 +245,7 @@ class Config:
|
|||
"""
|
||||
listed = [str(p).strip().lower() for p in self.get("auth", "providers", [])]
|
||||
out: list[str] = []
|
||||
for name in ("pin", "jwt", "oidc"):
|
||||
for name in ("mesh", "pin", "jwt", "oidc"):
|
||||
if name in listed or self.configured(name):
|
||||
out.append(name)
|
||||
# Rekkefølgen i config vinner for de som er nevnt der.
|
||||
|
|
@ -247,8 +260,16 @@ class Config:
|
|||
return bool(s.get("secret") or s.get("jwks_url"))
|
||||
if provider == "oidc":
|
||||
return bool(s.get("client_id") and (s.get("issuer") or s.get("authorization_endpoint")))
|
||||
if provider == "mesh":
|
||||
return bool(str(self.get("mesh", "secret", "")).strip())
|
||||
return False
|
||||
|
||||
@property
|
||||
def zones(self) -> dict[str, str]:
|
||||
"""Sonene på en sentral, i konfigurert rekkefølge. Tom = ingen mesh."""
|
||||
raw = self.section("zones")
|
||||
return {str(k): str(v).strip() for k, v in raw.items() if str(v).strip()}
|
||||
|
||||
def permissions(self, roles: tuple[str, ...]) -> frozenset[str]:
|
||||
"""Rollenavn -> handlinger. "*" betyr alt."""
|
||||
out: set[str] = set()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue