Planlagte sendinger

«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>
This commit is contained in:
Kent 2026-10-08 16:29:16 +02:00
parent 6c1253f54c
commit 04b3712ef5
15 changed files with 746 additions and 15 deletions

View file

@ -26,7 +26,7 @@ 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", "stop", "clear")
ACTIONS = ("view", "preview", "announce", "schedule", "stop", "clear")
DEFAULTS: dict[str, Any] = {
"app": {
@ -46,6 +46,7 @@ DEFAULTS: dict[str, Any] = {
"chimes": str(HERE / "chimes"),
"log": str(HERE / "logs" / "announcements.jsonl"),
"state": str(HERE / "instance"),
"schedule": str(HERE / "data" / "scheduled.json"),
},
"audio": {
# pw-play --target / paplay --device. Tom = systemets standardutgang.
@ -61,6 +62,16 @@ DEFAULTS: dict[str, Any] = {
# Full URL utenfra, trengs for OIDC redirect_uri bak en proxy.
"public_url": "",
},
"schedule": {
# Slingringsmonn etter planlagt tid. Tråden våkner med noen sekunders
# mellomrom, og en kjapp omstart skal ikke miste en sending. Mer enn
# dette for sent regnes som gått glipp av, og sendes ikke – et utrop
# om noe som skjedde for en time siden er feil å spille.
"grace_seconds": 120,
"max_days_ahead": 30,
# Hvor ofte vi ser etter forfalte sendinger.
"tick_seconds": 20,
},
"auth": {
# Tom liste = alt åpent. Se active_providers(): en leverandør er også
# aktiv når den er ferdig konfigurert, så det holder å sette
@ -105,7 +116,7 @@ DEFAULTS: dict[str, Any] = {
},
"roles": {
"anonymous": [],
"crew": ["view", "preview", "announce"],
"crew": ["view", "preview", "announce", "schedule"],
"admin": ["*"],
},
}
@ -131,6 +142,7 @@ ENV: dict[str, tuple[tuple[str, str], Callable[[str], Any]]] = {
"ANNOUNCER_CACHE": (("paths", "cache"), str),
"ANNOUNCER_CHIMES": (("paths", "chimes"), str),
"ANNOUNCER_LOG": (("paths", "log"), str),
"ANNOUNCER_SCHEDULE": (("paths", "schedule"), str),
"ANNOUNCER_STATE": (("paths", "state"), str),
"ANNOUNCER_SINK": (("audio", "sink"), str),
"ANNOUNCER_HOST": (("server", "host"), str),