«Send denne kl. 18:00.» Engangs, ikke gjentakende. Dette er den første delen av appen som må ligge på disk. Kø og historikk har alltid bare levd i minnet, og det har gått bra fordi en melding spilles med en gang. En melding planlagt til 18:00 må overleve at containeren startes om 17:45, så schedule.py lagrer i data/scheduled.json – skrevet til side og flyttet på plass, så fila aldri er halvskrevet. Ingen database, med vilje: dette er en håndfull meldinger på et arrangement, og en fil man kan lese med cat er verdt mer her enn indekser. En ødelagt fil hindrer ikke oppstart; da er det bedre å miste planene og si det i loggen. Modulen vet ingenting om Flask eller avspilling. Den lagrer, og den svarer på «hva skal sendes nå?». _scheduler() i announcer.py eier tråden som spør hvert 20. sekund og legger forfalte meldinger i den vanlige køen – derfra oppfører de seg nøyaktig som en melding sendt for hånd. Gikk den glipp av, spilles den ikke. due() skiller på grace_seconds (120): innenfor den sendes meldingen, utenfor markeres den missed og vises som det. Nådefristen er slingringsmonn for at tråden våkner med mellomrom og for en kjapp omstart, ikke en policy om å ta igjen etterslep – «turneringen starter om ti minutter» skal ikke ut på anlegget en time etter at maskina kom tilbake. due() endrer ingenting selv; markeringen skjer først etter at meldingen faktisk er i kø, så ingenting kan stå som «sendt» uten å ha blitt det. Klienten sender absolutt tidspunkt (epoch), så tidssoner spiller ingen rolle for selve planleggingen. TZ settes likevel fra .env, ellers står loggen i UTC mens man selv står i CEST. Planlegging er låst til samme forhåndslytt-nøkkel som «Send ut»: man planlegger bare det man har hørt. Det gir en bonus – forhåndslytt varmer cache/, så WAV-en ligger ferdig på disk, og en planlagt edge-tts-melding spiller kl. 18:00 selv om nettet er nede da. Sine egne kan man alltid avbryte. Andres krever clear, samme rett som å tømme køen, siden det er samme slags inngrep i noe andre har satt i gang. 23 nye tester. Verifisert i container: sending fyrer og spiller, overlever en omstart av containeren, og en melding som forfalt mens maskina var av blir markert missed uten å bli spilt. Merk ved oppgradering: schedule er en ny handling, og et oppsett med eksplisitt [roles] i config.toml får den ikke automatisk. Den må legges inn selv. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.4 KiB
Koble announcer til crew-panelet
Announcer har ingen brukerdatabase og skal ikke ha det. Den stoler på crew-panelet og leser identitet og roller ut av det panelet sier. Dette dokumentet er kontrakten: hva panelet må levere, og i hvilken rekkefølge det er verdt å bygge det.
Panelet har i dag verken JWT eller OIDC. Bygg JWT først – det er noen titalls linjer i panelet og dekker behovet. OIDC kan komme etterpå uten at noe i announcer må skrives om; begge veier ender i samme kode.
Trinn 1: JWT (anbefalt start)
Panelet signerer et kortlevd token som sier hvem brukeren er og hvilke roller han har. Announcer verifiserer signaturen og stoler på innholdet.
Hva tokenet skal inneholde
{
"iss": "https://crew.example.no",
"aud": "announcer",
"sub": "142",
"name": "Kari Nordmann",
"roles": ["crew"],
"exp": 1760000000
}
| Claim | Krav | Merknad |
|---|---|---|
iss |
må stemme med auth.jwt.issuer |
sjekkes bare hvis den er satt i config |
aud |
må stemme med auth.jwt.audience |
bruk "announcer" |
sub |
påkrevd i praksis | stabil bruker-id; havner i loggen som jwt:<sub> |
exp |
påkrevd | announcer avviser tokens uten utløpstid |
name |
valgfri | vises i køen. Faller tilbake på preferred_username, email, så sub |
roles |
valgfri | se under |
roles kan være en liste (["crew"]), en streng ("crew"), mellomroms- eller
kommadelt ("crew admin"), og den kan ligge nestet – da peker du på den med
role_claim = "realm_access.roles". Mangler den helt, får brukeren
auth.jwt.default_role, eller ingen rettigheter hvis den ikke er satt.
Rollenavnene betyr ingenting for announcer i seg selv; [roles] i config.toml
bestemmer hva de får lov til.
Signering
To varianter. Begge virker, den andre er bedre:
HS256 med delt hemmelighet – enklest. Samme streng i panelet og i announcer. Ulempen er at announcer dermed har en nøkkel som kan lage gyldige tokens.
[auth.jwt]
issuer = "https://crew.example.no"
audience = "announcer"
# settes som ANNOUNCER_JWT_SECRET i systemd-enheten, ikke her
RS256 med JWKS – panelet signerer med sin private nøkkel og publiserer den
offentlige på /.well-known/jwks.json. Announcer henter den og cacher den en
time. Da kan announcer bare verifisere, aldri signere, og nøkkelrullering
skjer av seg selv.
[auth.jwt]
issuer = "https://crew.example.no"
audience = "announcer"
jwks_url = "https://crew.example.no/.well-known/jwks.json"
Hvordan brukeren kommer inn
To måter, og panelet kan bruke begge:
Lenke (det du vil ha for et crew-medlem). Panelet lager en knapp «Åpne announcer» som peker på
http://<announcer>:8090/auth/token?t=<jwt>
Announcer verifiserer tokenet, setter en sesjonscookie og sender brukeren til
forsida. Ett klikk, ingen PIN å huske. Lag tokenet kortlevd (exp et par
minutter fram) – det er bare en billett inn; sesjonen varer så lenge tokenets
exp sier, eller session_seconds hvis det er lengre.
Header (for skript og integrasjoner). Send tokenet på hver forespørsel:
curl -X POST http://announcer:8090/api/announce \
-H "Authorization: Bearer $JWT" \
-H 'Content-Type: application/json' \
-d '{"text":"Testmelding","voice":"finn"}'
Minimum i panelet (Python-eksempel)
import time, jwt # pyjwt
def announcer_url(user) -> str:
token = jwt.encode(
{
"iss": "https://crew.example.no",
"aud": "announcer",
"sub": str(user.id),
"name": user.full_name,
"roles": ["admin" if user.is_crew_lead else "crew"],
"exp": time.time() + 120,
},
ANNOUNCER_JWT_SECRET,
algorithm="HS256",
)
return f"http://announcer.lan:8090/auth/token?t={token}"
Trinn 2: OIDC
Når panelet har fått authorize-, token- og gjerne userinfo-endepunkter,
kan announcer bli en vanlig OIDC-klient. Da logger brukeren inn i panelet i
nettleseren og trenger ingen lenke med token i.
Announcer bruker autorisasjonskode med PKCE (S256). Dette må registreres i panelet:
client_id |
f.eks. announcer |
redirect_uri |
http://<vert>:8090/auth/callback – eksakt, inkludert port |
grant_type |
authorization_code |
scope |
openid profile email (eller bare openid) |
| PKCE | S256 |
Announcer leser {issuer}/.well-known/openid-configuration ved første
innlogging. Har ikke panelet discovery, settes endepunktene direkte:
[auth.oidc]
issuer = "https://crew.example.no"
client_id = "announcer"
authorization_endpoint = "https://crew.example.no/oauth/authorize"
token_endpoint = "https://crew.example.no/oauth/token"
jwks_url = "https://crew.example.no/.well-known/jwks.json"
client_secret settes som ANNOUNCER_OIDC_CLIENT_SECRET. Rollene leses ut av
id_token på samme måte som over.
Står announcer bak en omvendt proxy, sett server.public_url så
redirect_uri blir riktig, og server.behind_tls = true.
Roller og rettigheter
Fem handlinger. config.toml kobler rollenavn til dem:
| Handling | Hva den gir |
|---|---|
view |
se køen og hva som spilles nå |
preview |
forhåndslytte (koster en syntese) |
announce |
sende ut på anlegget |
schedule |
planlegge en sending fram i tid, og avbryte sine egne |
stop |
stoppe det som spilles |
clear |
tømme køen (og se IP-adresser i køen) |
[roles]
anonymous = [] # ["view"] for en statusskjerm
crew = ["view", "preview", "announce", "schedule"]
admin = ["*"]
Endrer du [roles] slår det inn ved neste forespørsel – ingen må logge inn på
nytt. (Nye [auth.*]-verdier krever omstart.)
Hva announcer svarer
| Kode | Betyr | Hva klienten skal gjøre |
|---|---|---|
| 401 | vi vet ikke hvem du er | logg inn |
| 403 | vi vet hvem du er, rollen din får ikke dette | ikke prøv igjen |
GET /api/config svarer alltid 200 og forteller i auth-blokka hvilke metoder
som er aktive, hvem du er og hva du får gjøre. Det er den sida bruker for å
vise eller skjule knapper.
Loggen
Hvert utrop havner i logs/announcements.jsonl, én JSON-linje per hendelse:
{"ts":1760000000.12,"action":"announce","subject":"jwt:142","name":"Kari Nordmann",
"method":"jwt","ip":"10.20.70.55","id":7,"text":"Pizzaen er kommet","voice":"finn"}
action er announce, played, stop eller clear. Fila rullerer til
.jsonl.1 ved 5 MB.