Lazy Termin: Terminbenachrichtigungen, die nie doppelt kommen
Ein Selenium-Checker, der eine deutsche Terminbuchungsseite überwacht und verifizierte Abonnenten über eine PostgreSQL-Outbox per Telegram benachrichtigt.
Überblick
- Rolle
- Alleinentwickler
- Zeitraum
- Aug. 2026 – heute
- Technologien
- Python · Selenium · PostgreSQL · python-telegram-bot · Amazon SES · Docker Compose · GitHub Actions
41
Unit-Tests gegen eine echte PostgreSQL
≤1
Meldung pro Abonnent und Sperrfrist
Standard-Sperrfrist: 60 Minuten
3
Zustellversuche, bevor der Admin informiert wird
Auf dieser Seite
Problem
In Deutschland vergeben viele Behörden Termine über Online-Buchungsseiten. Freie Termine tauchen unvorhersehbar auf und sind schnell weg – wer einen will, muss denselben mehrseitigen Buchungsablauf immer wieder durchklicken. Lazy Termin übernimmt das Prüfen und meldet sich, sobald ein Termin frei wird. Gebucht wird dann selbst auf der offiziellen Seite.
Rahmenbedingungen
- Rücksichtsvoll bleiben. Der Checker nutzt den normalen Buchungsablauf, wartet zwischen den Schritten zufällig ein bis drei Sekunden und umgeht nie CAPTCHAs, Rate Limits oder Logins. Er bucht nie selbst. Geplante Läufe finden alle 15 Minuten zu Bürozeiten an Werktagen statt (Europe/Berlin).
- Keine Meldung verlieren, keine doppelt senden. Checker und Telegram-Bot sind getrennte Prozesse, und beide können jederzeit neu starten.
- Kein Spam. Jeder Abonnent erhält höchstens eine Meldung pro Sperrfrist, standardmäßig 60 Minuten.
- Kleiner, echter Nutzerkreis. Für ein Abo braucht es eine E-Mail-Adresse auf einer erlaubten Domain, bestätigt per Einmalcode über Amazon SES. Abos sind begrenzt und laufen nach fünf Tagen ab.
- So wenig Daten wie möglich. Die Nachrichtentelemetrie speichert Zeitstempel, Telegram-Nutzer-ID und Nachrichtentyp – nie die Nachricht selbst.
Vorgehen und Architektur
cron in Docker Compose
Zeitplan
Alle 15 Minuten, werktags 07:00–16:45, Europe/Berlin
GitHub Actions
Zeitplan
Werktägliche Zeitfenster oder manuell gestartet
Selenium-Buchungsablauf
Checker
Headless Chrome · zufällig 1–3 s Pause zwischen den Schritten
- Termin gefunden → INSERT + NOTIFY
notification_outbox
PostgreSQL
Höchstens eine wartende APPOINTMENT_FOUND-Zeile
- LISTEN oder Abfrage alle 20 s
Mit SKIP LOCKED beanspruchen
Bot
Abonnenten außerhalb ihrer Sperrfrist auswählen
Jeden berechtigten Abonnenten benachrichtigen
Telegram
Jede Zustellung in alert_delivery protokolliert · 3 Fehlversuche → Admin
Beschreibung des Diagramms
Ein Scheduler startet den Selenium-Checker, entweder per cron in Docker Compose oder über GitHub Actions. Findet der Checker einen freien Termin, fügt er eine APPOINTMENT_FOUND-Zeile in die Benachrichtigungs-Outbox ein, sofern noch keine wartet, und sendet NOTIFY. Der Telegram-Bot wacht durch die Benachrichtigung auf oder fragt alle 20 Sekunden ab, beansprucht wartende Zeilen mit SKIP LOCKED und benachrichtigt jeden Abonnenten außerhalb seiner Sperrfrist, wobei jede Zustellung protokolliert wird. Nach drei Fehlversuchen wird eine Zeile als fehlgeschlagen markiert und der Admin informiert.
/subscribe
Telegram
Name, dann eine E-Mail-Adresse
Domain- und Kapazitätsprüfung
Bot
Nur erlaubte Domains, begrenzte Zahl aktiver Abos
Einmalcode per E-Mail
Amazon SES
/confirm
Telegram
Fünf Tage abonniert, danach automatisch abgelaufen
Beschreibung des Diagramms
Ein Nutzer sendet /subscribe an den Telegram-Bot und gibt einen Namen und eine E-Mail-Adresse ein. Der Bot prüft, ob die Domain erlaubt und das Abo-Limit nicht erreicht ist, und sendet dann einen Einmalcode über Amazon SES. Nach Bestätigung des Codes ist das Abo fünf Tage lang aktiv.
Entscheidung
Eine transaktionale Outbox in PostgreSQL
Der Checker fügt nur eine APPOINTMENT_FOUND-Zeile ein (sofern noch keine wartet), und die Zustellung liegt beim Bot. Jeder Prozess kann ausfallen, ohne dass der andere es merkt.
Ebenfalls erwogen
- Die Telegram-API direkt aus dem Checker aufrufenEine Meldung während eines Bot-Ausfalls oder eines Absturzes mitten in der Schleife geht verloren oder kommt doppelt.
Entscheidung
Den Bot per LISTEN/NOTIFY wecken, mit Abfrage alle 20 Sekunden als Rückfallebene
NOTIFY macht die Zustellung nahezu sofortig, und die Abfrageschleife fängt alles auf, was eine abgebrochene Verbindung verpasst.
Ebenfalls erwogen
- Ein Message Broker wie Redis oder RabbitMQEin weiterer Dienst für eine einzige Warteschlange, die PostgreSQL bereits bietet.
Entscheidung
Zeilen mit SKIP LOCKED beanspruchen und jede Zustellung protokollieren
Jede Zustellung wird in die Tabelle alert_delivery geschrieben, sodass eine wiederholte Zeile bereits benachrichtigte Abonnenten überspringt. Nach drei Fehlversuchen wird die Zeile als failed markiert und der Admin informiert.
Ebenfalls erwogen
- Bei Fehlern den gesamten Versand wiederholenAlle, die die Meldung schon erhalten haben, bekämen sie erneut.
Entscheidung
Einen echten Browser mit Selenium steuern
Headless Chrome ohne Bilder hält den Speicherbedarf niedrig. Wird ein Klick abgefangen, hält ein JavaScript-Klick als Rückfallebene den Ablauf in Gang.
Ebenfalls erwogen
- Den Buchungsablauf mit einfachen HTTP-Anfragen nachspielenDer Ablauf erstreckt sich über mehrere Seiten, und ihn nachzuspielen hieße, seinen Zustand von Hand nachzubauen.
Ergebnisse
41
Unit-Tests gegen eine echte PostgreSQL-Datenbank
Outbox-Claims, Wiederholungen, Wiederherstellung hängender Zeilen, Sperrfristen, Abo-Ablauf und Selenium-Rückfallebenen
≤1
Meldung pro Abonnent und Sperrfrist
Durchgesetzt über die Tabelle alert_delivery
30 Tage
bis Outbox- und Telemetriezeilen gelöscht werden
Nächtlich vom Bot bereinigt
Version 1.0.0 ist auf GitHub veröffentlicht. Sie läuft unter Docker Compose mit Heartbeat-Healthcheck oder zeitgesteuert in GitHub Actions.
Was ich heute anders machen würde
- Einen kaputten Buchungsablauf als eigene Meldung behandeln. Die Selektoren hängen von der Zielseite ab, und eine stille Layoutänderung sollte nie wie „heute keine Termine“ aussehen.
- Die Zeit bis zur Meldung messen, vom Erscheinen des Termins bis zum Eintreffen der Nachricht. Das ist die eine Zahl, die Abonnenten interessiert.