Commit graph

6 commits

Author SHA1 Message Date
7c521f8d1f Mesh: sonene melder seg selv; mDNS på LAN, NetBird-provisjonering
Sonene trenger ikke stå i [zones] på sentralen. En sone sender hjerteslag
hvert 30. s (POST /api/mesh/register med navn, egen adresse og
mesh-nøkkelen), dukker opp i lista med en gang, og tas ut etter 90 s uten
slag. Riktig nøkkel er godkjenningen. Statisk [zones] vinner over et navn
som melder seg (409) – en skrivefeil i config skal ikke kunne overstyres av
en boks på nettet.

Hvorfor push og ikke pull fra NetBird: netbird status --json gir navn, IP og
status per peer, men ikke grupper. Pull ville krevd en service-token mot
management-serveren og virket bare over NetBird. Push virker likt over
NetBird og LAN, uten token, og gir liveness gratis.

Sonen finner sentralen lagvis: ANNOUNCER_SENTRAL hvis satt (NetBird-DNS-navn,
konvensjon announcer-<navn>.skynet.corp), ellers mDNS (_announcer._tcp,
discovery.py). Egen adresse regnes ut som ruten mot sentralen – over NetBird
blir det wt0-adressen, på LAN LAN-adressen – med ANNOUNCER_URL som
overstyring. Hjerteslag hvert 5. s til første OK, så oppstart ikke tar et
halvt minutt.

Containeren bruker nå vertens nett. mDNS-multicast når ikke LAN-et fra
bridge-nettet, så det var ingen vei utenom – og det ga tilbake ufw: det
gamle Docker-smutthullet er borte, portregler virker igjen, og BIND_IP er
appens eget --host. Appen lytter alltid også på 127.0.0.1 for helsesjekken.

install.sh tar NETBIRD_SETUP_KEY: installerer NetBird, melder maskina inn
som announcer-<navn>, venter på wt0, binder til NetBird-adressen.
Management-URL leses fra /etc/netbird/config.json når den finnes.

Rollen avgjøres av config (Config.role): sentral hvis [zones] eller
ANNOUNCER_ROLE=sentral, sone hvis bare nøkkelen er satt, ellers som før.
Dispatcher og ZonePoller slår opp soner ved hver bruk, siden de kommer og
går. register følger, som play, aldri med «alt åpent». Flask sorterte
JSON-nøkler alfabetisk, så sonene kom i feil rekkefølge i /api/status –
app.json.sort_keys = False.

25 nye tester, 173 totalt. Verifisert i compose på host-nett: to soner
meldte seg innen 4 s uten noe [zones], fordeling til en av dem havnet i kø,
avahi-browse på verten ser sentralen, en stoppet sone ble tatt ut etter 90 s
og var tilbake 2 s etter start. Underveis fant live-testen at testkonfigen
fortsatt hadde statiske a/b – og sentralen avviste hjerteslagene med 409,
som den skal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 02:50:25 +02:00
eb27a162d5 Gjennomgang: Stopp under syntese, planstatus, waitress, tester på HTTP-laget
Sju ting fra en gjennomgang av det som ble bygd i går.

Stopp virket ikke mens lyden ble laget. Player.stop() drepte self.proc, men
under syntesen finnes ingen prosess ennå – det er de første sekundene av hver
edge-tts-melding – så knappen gjorde ingenting, og meldingen gikk ut etterpå.
Nå har køen statusen preparing, stop() setter stopped på den, og run_once()
sjekker det før den starter avspilling. UI-et viser «Lager lyd …» i stedet
for å late som det spilles i stillhet.

Planlagte sendinger løy om status. De ble markert «sendt» idet de ble lagt i
kø, og ble stående slik selv om avspillingen feilet. Player har fått en
on_finished-callback, og planen følger nå meldingen helt i mål: sent, failed
eller stopped – det som faktisk skjedde. Statusene i schedule.py er utvidet
tilsvarende (queued er «lagt i kø», sent er «spilt ferdig»).

Flask sin utviklingsserver er byttet ut med waitress. Én prosess med mange
tråder, med vilje: Player og planleggeren er singletons, så gunicorn med
flere workere ville gitt flere køer som snakker i munnen på hverandre.

HTTP-laget hadde null tester. import announcer startet tråder og dro inn
Piper, så rutene lot seg ikke prøve. Trådene startes nå i main(), og
run_once() driver køen ett steg om gangen. tests/test_routes.py dekker
rutene, valideringen, køen, Stopp i begge faser, revisjonsloggen og hele
planleggingsflyten gjennom HTTP. 30 nye tester, 110 totalt.

Kappløp i synthesize(): tmp-fila hadde fast navn, så to som forhåndslyttet
samme tekst samtidig skrev til samme fil, og den ene rename feilet. Unik
tmp-fil per kall.

verify_signature = false er fjernet. Det var en felle: én feilskrevet linje
i config, og alle tokens ble godtatt. Ingen trenger den.

docker compose up uten install.sh krasjet kryptisk ved første utrop, fordi
Docker lagde data/ som root. check_writable() sier nå klart fra ved oppstart,
med hva som må gjøres.

Verifisert live i container: Stopp under edge-tts-syntese gir stopped, ikke
done. Server-headeren er waitress sin, ikke werkzeugs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 01:41:31 +02:00
d6eb811ee5 Pluggbar autentisering: PIN, JWT og OIDC
Dagens «auth» var én delt PIN i et X-Pin-header, sammenlignet med !=, og
den sa ingenting om hvem som sendte et utrop – køen viste bare IP-adressen.
Crew har et eget adminpanel som skal bli identitetsleverandør.

Modellen er én datatype og én funksjon: Principal (hvem + roller +
rettigheter) og require(action), som enten gir deg den eller stopper
forespørselen. Handlinger: view, preview, announce, stop, clear.

Tre leverandører, prøvd i rekkefølge, alle valgfrie:

- pin   X-Pin som før, men med hmac.compare_digest istedenfor ==. Rolle
        admin som standard, så eksisterende oppsett oppfører seg likt.
        Beholdt bevisst: den virker uten nett, og det er nettopp da man
        trenger å rope ut noe.
- jwt   Authorization: Bearer. HS256 med delt hemmelighet, eller
        RS256/ES256 mot panelets JWKS. Krever exp – et token uten
        utløpstid er en evig nøkkel. Roller fra et konfigurerbart claim,
        som tåler liste, streng, kommaliste og punktsti.
- oidc  Autorisasjonskode med PKCE. Discovery hentes først ved første
        innlogging, så tjenesten starter selv om panelet er nede.

En leverandør er aktiv når den står i auth.providers *eller* når den er
ferdig konfigurert. Derfor slår ANNOUNCER_PIN=1234 alene fortsatt på PIN,
slik det har virket til nå. Er ingen aktiv, er alt åpent – som før.

/auth/token?t=<jwt> er broa mens panelet mangler OIDC: panelet lenker hit
med et kortlevd token, vi setter sesjonscookie. Ett klikk, ingen PIN.
Kontrakten panelet skal oppfylle står i docs/crew-integration.md.

401 betyr «vi vet ikke hvem du er, logg inn», 403 «vi vet hvem du er, men
rollen din får ikke dette». Et gyldig token uten roller gir 403, ikke 401 –
å logge inn på nytt ville ikke hjulpet.

Samtidig:

- /api/status er lukket bak «view». Den var åpen, og lekket både
  meldingstekstene og IP-ene til alle som hadde sendt noe, til hvem som
  helst som nådde porten. IP vises nå bare for den som har «clear»; alle
  andre ser navnet.
- Køen og en revisjonslogg i logs/announcements.jsonl viser hvem som sendte
  hva. Loggen feiler aldri forespørselen – en logg som ikke lar seg skrive
  skal ikke hindre et utrop.
- Branding flyttet fra index.html til /api/config, så config.toml fra
  forrige commit faktisk brukes.
- Sesjonsnøkkel i instance/secret_key, ellers logges alle ut ved hver
  restart. SESSION_COOKIE_SECURE settes bare bak TLS: appen kjører på rå
  HTTP over LAN og NetBird, og med Secure på ville ingen klart å logge inn.

57 tester, de fleste på auth, inkludert hele OIDC-runden mot en stubbet
leverandør. Det er delen man ikke kan se etter med øyet: at en PIN virker
ser du med en gang, at et utløpt token blir avvist ser du aldri før det
smeller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 15:43:35 +02:00
f191fb2b90 Døp om sg-announcer til announcer
Appen var navngitt etter Sauda Gathering, men det er ingenting
arrangementsspesifikt i selve koden. Navnet står igjen i tjenesten,
install.sh og pyproject.

- install.sh skriver nå announcer.service, og rydder bort en gammel
  sg-announcer.service først. Uten det slåss to enheter om port 8090
  når skriptet kjøres på en maskin som har den gamle.
- Enhetsfila får chmod 600; PIN-en står i klartekst i den.
- src/sg_announcer/ er slettet. Det var en uv-init-stub som bare printet
  «Hello from sg-announcer!», pekt på av [project.scripts], og som ingen
  har kalt noen gang.
- [tool.uv] package = false: appen er løse .py-filer som kjøres på plass,
  ikke en pakke. Uten den vil uv sync bygge et wheel og klage på at
  announcer/__init__.py mangler.

Repoet i Forgejo og mappa er døpt om tilsvarende.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 15:42:36 +02:00
45a22e7ed6 edge-tts som standardmotor (Finn/Pernille), Piper som lokal reserve
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 03:42:16 +02:00
1c3e17afa0 SG Announcer: Piper-TTS-utroper med forhåndslytt og kø
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 03:18:13 +02:00