announcer/install.sh
Kent 04b3712ef5 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>
2026-10-08 16:29:16 +02:00

172 lines
6.8 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Setter opp Announcer på klienten som er koblet til høyttaleranlegget.
# Debian/Ubuntu/Mint. Appen kjører i Docker; dette skriptet ordner Docker,
# .env, datamapper, brannmur og autostart.
#
# git clone https://git.1x1.no/kent/announcer.git
# cd announcer && ./install.sh
#
# Kjør som vanlig bruker (ikke sudo); skriptet spør om passord der det trengs.
# Valgfritt:
# ANNOUNCER_PIN=1234 ./install.sh sida krever PIN
# ANNOUNCER_PORT=8090 ./install.sh annen port
# ANNOUNCER_TITLE="Utrop" ./install.sh annen tittel
# BIND_IP=127.0.0.1 ./install.sh bare maskina selv
#
# Kan kjøres på nytt; da bygges bildet om og containeren startes om. En .env
# som finnes blir ikke overskrevet.
set -euo pipefail
if [[ $EUID -eq 0 ]]; then
echo "Kjør som vanlig bruker, ikke root/sudo." >&2
exit 1
fi
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$REPO_DIR"
step() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
warn() { printf '\033[33m! %s\033[0m\n' "$*"; }
# --------------------------------------------------------------------------
step "Rydder bort eventuell nativ installasjon"
# Tidligere versjoner kjørte appen direkte som en systemd-brukerenhet. To
# instanser ville slåss om porten, så den gamle skal bort.
for old in sg-announcer announcer; do
unit="$HOME/.config/systemd/user/$old.service"
if [[ -f "$unit" ]]; then
systemctl --user disable --now "$old" >/dev/null 2>&1 || true
rm -f "$unit"
echo "fjernet $old.service (nativ)"
fi
done
systemctl --user daemon-reload 2>/dev/null || true
# --------------------------------------------------------------------------
step "Docker"
if ! command -v docker >/dev/null 2>&1; then
echo "installerer Docker …"
curl -fsSL https://get.docker.com | sudo sh
fi
if ! id -nG "$USER" | tr ' ' '\n' | grep -qx docker; then
sudo usermod -aG docker "$USER"
warn "La $USER i docker-gruppa. Du må logge ut og inn igjen,"
warn "og så kjøre ./install.sh på nytt. Stopper her."
exit 0
fi
if ! docker info >/dev/null 2>&1; then
warn "Docker svarer ikke. Er tjenesten startet? sudo systemctl enable --now docker"
exit 1
fi
docker --version
docker compose version | head -1
# --------------------------------------------------------------------------
step "Oppsett (.env)"
# BIND_IP er det som avgjør hvem som når sida. ufw gjør det ikke: Dockers
# publiserte porter settes opp i nat-tabellen og går forbi ufws regler.
NB_IP="$(ip -4 addr show wt0 2>/dev/null | awk '/inet /{sub(/\/.*/,"",$2);print $2;exit}')"
if [[ -n "${BIND_IP:-}" ]]; then
:
elif [[ -n "$NB_IP" ]]; then
BIND_IP="$NB_IP"
echo "NetBird funnet – binder til $BIND_IP, så bare VPN-et når sida."
else
BIND_IP="0.0.0.0"
fi
if [[ -f .env ]]; then
echo ".env finnes allerede, lar den stå. Slett den for å lage en ny."
else
cat > .env <<ENV
# Laget av install.sh $(date +%Y-%m-%d). Se .env.example for forklaringer.
PUID=$(id -u)
PGID=$(id -g)
XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
BIND_IP=$BIND_IP
TZ=$(timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo Europe/Oslo)
ANNOUNCER_PORT=${ANNOUNCER_PORT:-8090}
ANNOUNCER_SINK=${ANNOUNCER_SINK:-}
ANNOUNCER_TITLE=${ANNOUNCER_TITLE:-}
ANNOUNCER_PUBLIC_URL=${ANNOUNCER_PUBLIC_URL:-}
ANNOUNCER_PIN=${ANNOUNCER_PIN:-}
ANNOUNCER_JWT_SECRET=${ANNOUNCER_JWT_SECRET:-}
ANNOUNCER_OIDC_CLIENT_SECRET=${ANNOUNCER_OIDC_CLIENT_SECRET:-}
ANNOUNCER_SECRET_KEY=
ENV
chmod 600 .env # PIN og nøkler står i klartekst
echo "skrev .env (PUID=$(id -u), BIND_IP=$BIND_IP)"
fi
step "Datamapper"
mkdir -p data/{voices,cache,chimes,logs,instance}
[[ -f data/chimes/README.txt ]] || cp -n chimes/README.txt data/chimes/ 2>/dev/null || true
echo "data/ – stemmer, buffer, egne jingler, logg og sesjonsnøkkel"
if [[ ! -f data/config.toml ]]; then
echo "ingen data/config.toml: sida er åpen for alle som når den."
echo " vil du ha innlogging: cp config.example.toml data/config.toml"
fi
# --------------------------------------------------------------------------
step "Bygger og starter"
docker compose up -d --build
step "Venter på at tjenesten svarer"
# Første oppstart laster ned Piper-stemmene (~670 MB) før den svarer.
PORT="$(grep -E '^ANNOUNCER_PORT=' .env | cut -d= -f2)"
PROBE_IP="$BIND_IP"; [[ "$PROBE_IP" == "0.0.0.0" ]] && PROBE_IP=127.0.0.1
for i in $(seq 1 60); do
if curl -fsS -o /dev/null "http://$PROBE_IP:$PORT/api/config" 2>/dev/null; then
echo "svarer etter ~$((i*5)) s"
break
fi
[[ $i -eq 1 ]] && echo "laster ned stemmer, dette tar noen minutter første gang …"
[[ $i -eq 60 ]] && warn "svarte ikke innen 5 min. Se: docker compose logs -f"
sleep 5
done
# --------------------------------------------------------------------------
step "Brannmur"
# Merk at ufw *ikke* styrer tilgangen til announcer – det gjør BIND_IP i
# .env. ufw settes her bare for maskinens øvrige porter.
if [[ -n "$NB_IP" ]]; then
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on wt0 to any port 22 proto tcp comment "ssh via netbird"
sudo ufw allow in on wt0 to any port 22022 proto tcp comment "netbird ssh-server"
sudo ufw --force enable
echo "ufw aktiv: ssh kun på wt0."
echo "announcer er bundet til $BIND_IP:$PORT – det er NetBird-adressen,"
echo "så sida nås bare over VPN-et. ufw er ikke involvert i det."
elif command -v ufw >/dev/null 2>&1 && sudo ufw status 2>/dev/null | grep -q "Status: active"; then
warn "ufw er aktiv, men styrer ikke Docker-porter."
warn "Vil du begrense tilgangen: sett BIND_IP i .env og kjør docker compose up -d."
else
echo "ufw er ikke aktiv."
[[ "$BIND_IP" == "0.0.0.0" ]] && warn "Sida er åpen på alle nett maskina er på."
fi
# --------------------------------------------------------------------------
IP="$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{sub(/\/.*/,"",$2);print $2;exit}')"
step "Ferdig"
docker compose ps
cat <<MSG
Åpne http://${BIND_IP/0.0.0.0/${IP:-<denne-maskinens-ip>}}:$PORT
Lyden spilles i din innloggede økt, så maskinen må være logget inn grafisk.
Containeren starter ved boot, altså før innlogging – da feiler avspilling,
men den retter seg selv ved første utrop etter at du er logget inn.
Tips for en laptop på arrangementet:
- Slå på automatisk innlogging (Innstillinger -> Påloggingsvindu).
- Hindre at den sover med lokket igjen:
sudo sed -i 's/^#\?HandleLidSwitch=.*/HandleLidSwitch=ignore/' /etc/systemd/logind.conf
sudo systemctl restart systemd-logind
Drift:
docker compose logs -f følg loggen
docker compose restart etter endringer i data/config.toml
docker compose up -d --build etter endringer i koden
cat data/logs/announcements.jsonl hvem som sendte hva
MSG