Add watchdog for missed launchd runs, fix backup path bug + retention
Watchdog (scripts/utils/watchdog.py, com.lotto.watchdog.plist, daily 21:30) checks whether the latest draw or the trained models are older than the launchd schedule allows and sends a Telegram alert - this would have caught the Aug 15 update run that silently never fired (the Mac was presumably asleep; launchd does not retry missed StartCalendarInterval jobs), instead of that only surfacing on the next manual check. Also fixes a real path bug in LottoAPIUpdater.create_backup(): since self.data_file already points into data/, joining "data","backups" wrote backups to data/data/backups/ instead of data/backups/ - 73 backups (6.3MB) had been piling up there undetected for months, with data/data/ only ever gitignored rather than the root cause fixed. Merges the misplaced backups into the correct location and adds retention (keep last 30) so backups no longer grow unbounded; applied the same retention to Eurojackpot's updater in a matching commit there, which didn't have the path bug but had the same unbounded growth (85 backups, 2.4MB). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
427e704f3c
commit
612334dc1a
8 files changed
+178
-9899
No files matched your search
@@ -59,7 +59,6 @@ data/backups/
|
|||||||
data/*.backup*
|
data/*.backup*
|
||||||
data/generated_tips/weekly_lotto_tips_*.csv
|
data/generated_tips/weekly_lotto_tips_*.csv
|
||||||
data/performance_reports/
|
data/performance_reports/
|
||||||
data/data/
|
|
||||||
|
|
||||||
# graphify Code-Graph (regenerierbar via /graphify --update)
|
# graphify Code-Graph (regenerierbar via /graphify --update)
|
||||||
graphify-out/
|
graphify-out/
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "/Users/sebastianfrohlich/Projekte/Lotto"
|
||||||
|
source venv/bin/activate
|
||||||
|
python scripts/utils/watchdog.py
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.lotto.watchdog</string>
|
||||||
|
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Users/sebastianfrohlich/Projekte/Lotto/run_watchdog.sh</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>StartCalendarInterval</key>
|
||||||
|
<dict>
|
||||||
|
<key>Hour</key>
|
||||||
|
<integer>21</integer>
|
||||||
|
<key>Minute</key>
|
||||||
|
<integer>30</integer>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/Users/sebastianfrohlich/Projekte/Lotto</string>
|
||||||
|
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/sebastianfrohlich/Projekte/Lotto/logs/watchdog_stdout.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/sebastianfrohlich/Projekte/Lotto/logs/watchdog_stderr.log</string>
|
||||||
|
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<false/>
|
||||||
|
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>PATH</key>
|
||||||
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||||
|
<key>LANG</key>
|
||||||
|
<string>de_DE.UTF-8</string>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>LaunchOnlyOnce</key>
|
||||||
|
<false/>
|
||||||
|
|
||||||
|
<key>Nice</key>
|
||||||
|
<integer>10</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -265,6 +265,36 @@ class LottoNotifier:
|
|||||||
# TODO: Email-Versand implementieren wenn gewünscht
|
# TODO: Email-Versand implementieren wenn gewünscht
|
||||||
print("⚠️ Email-Benachrichtigung noch nicht implementiert")
|
print("⚠️ Email-Benachrichtigung noch nicht implementiert")
|
||||||
|
|
||||||
|
def send_alert(self, message: str) -> bool:
|
||||||
|
"""Sendet eine formlose Warn-/Alarm-Nachricht (z.B. vom Watchdog)."""
|
||||||
|
if not self.config['telegram']['enabled']:
|
||||||
|
print("⚠️ Telegram nicht aktiviert in config/notifications.json")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
bot_token = self.config['telegram']['bot_token']
|
||||||
|
chat_id = self.config['telegram']['chat_id']
|
||||||
|
|
||||||
|
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
||||||
|
data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"text": message,
|
||||||
|
"parse_mode": "Markdown"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, data=data, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print("✅ Alarm-Benachrichtigung gesendet")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Telegram-Fehler: {response.status_code}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Alarm-Benachrichtigung fehlgeschlagen: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def send_test_notification(self):
|
def send_test_notification(self):
|
||||||
"""Sendet Test-Benachrichtigung."""
|
"""Sendet Test-Benachrichtigung."""
|
||||||
if self.config['telegram']['enabled']:
|
if self.config['telegram']['enabled']:
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class LottoAPIUpdater:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def create_backup(self) -> bool:
|
def create_backup(self) -> bool:
|
||||||
"""Erstellt Backup."""
|
"""Erstellt Backup und behält nur die letzten KEEP_BACKUPS Stück."""
|
||||||
if not os.path.exists(self.data_file):
|
if not os.path.exists(self.data_file):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -99,7 +99,10 @@ class LottoAPIUpdater:
|
|||||||
print("\n💾 Erstelle Backup...")
|
print("\n💾 Erstelle Backup...")
|
||||||
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
backup_dir = os.path.join(os.path.dirname(self.data_file), "data", "backups")
|
# self.data_file liegt bereits unter .../data/ - vorher wurde hier
|
||||||
|
# zusätzlich "data" angehängt, was seit Monaten ungenutzt nach
|
||||||
|
# .../data/data/backups/ statt .../data/backups/ geschrieben hat.
|
||||||
|
backup_dir = os.path.join(os.path.dirname(self.data_file), "backups")
|
||||||
os.makedirs(backup_dir, exist_ok=True)
|
os.makedirs(backup_dir, exist_ok=True)
|
||||||
|
|
||||||
filename = os.path.basename(self.data_file)
|
filename = os.path.basename(self.data_file)
|
||||||
@@ -108,12 +111,28 @@ class LottoAPIUpdater:
|
|||||||
shutil.copy2(self.data_file, self.backup_file)
|
shutil.copy2(self.data_file, self.backup_file)
|
||||||
print(f" ✅ Backup: {os.path.basename(self.backup_file)}")
|
print(f" ✅ Backup: {os.path.basename(self.backup_file)}")
|
||||||
|
|
||||||
|
self._prune_old_backups(backup_dir, filename)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ⚠️ Backup-Fehler: {e}")
|
print(f" ⚠️ Backup-Fehler: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _prune_old_backups(backup_dir: str, filename: str, keep: int = 30) -> None:
|
||||||
|
"""Behält nur die KEEP neuesten Backups, löscht den Rest."""
|
||||||
|
pattern = f"{filename}.backup_"
|
||||||
|
backups = sorted(
|
||||||
|
(f for f in os.listdir(backup_dir) if f.startswith(pattern)),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
for old in backups[keep:]:
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(backup_dir, old))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
def fetch_from_api(self, api_name: str) -> List[Dict]:
|
def fetch_from_api(self, api_name: str) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Holt Daten von spezifischer API.
|
Holt Daten von spezifischer API.
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Watchdog für die Lotto-Automatisierung.
|
||||||
|
|
||||||
|
Prüft die Aktualität von Ziehungsdaten und trainierten Modellen anhand des
|
||||||
|
Ziehungskalenders (Mi + Sa) und meldet sich per Telegram, wenn etwas länger
|
||||||
|
stillsteht als der launchd-Zeitplan erlaubt. Das fängt genau den Fall ab, in
|
||||||
|
dem ein StartCalendarInterval-Job verpasst wurde (z.B. weil der Mac im Schlaf
|
||||||
|
war) - launchd holt das selbst nicht nach, und ohne diesen Check fällt es
|
||||||
|
sonst erst bei der nächsten manuellen Nachfrage auf.
|
||||||
|
|
||||||
|
Aufruf: python3 scripts/utils/watchdog.py
|
||||||
|
Gedacht für einen täglichen launchd-Job (siehe com.lotto.watchdog.plist).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from notifier import LottoNotifier
|
||||||
|
|
||||||
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
DATA_FILE = os.path.join(PROJECT_ROOT, "data", "AlleLottozahlen.csv")
|
||||||
|
MODEL_FILE = os.path.join(PROJECT_ROOT, "data", "ultimate_ml_models", "trained_models.pkl")
|
||||||
|
|
||||||
|
# Lotto zieht Mi + Sa -> maximal 4 Tage zwischen zwei Ziehungen. 5 Tage
|
||||||
|
# Toleranz lassen dem Update-Job (selber Abend, 20 Uhr) Luft, ohne echte
|
||||||
|
# Ausfälle zu verpassen.
|
||||||
|
MAX_DAYS_SINCE_DRAW = 5
|
||||||
|
|
||||||
|
|
||||||
|
def check() -> list[str]:
|
||||||
|
"""Prüft Daten- und Modell-Aktualität, gibt gefundene Probleme zurück."""
|
||||||
|
problems = []
|
||||||
|
|
||||||
|
df = pd.read_csv(DATA_FILE, sep=';')
|
||||||
|
last_draw = pd.to_datetime(df['datum'], format='%Y-%m-%d').max().date()
|
||||||
|
days_since_draw = (datetime.now().date() - last_draw).days
|
||||||
|
|
||||||
|
if days_since_draw > MAX_DAYS_SINCE_DRAW:
|
||||||
|
problems.append(
|
||||||
|
f"Letzte Ziehung in den Daten ist {last_draw.isoformat()} "
|
||||||
|
f"({days_since_draw} Tage her) - der automatische Update-Lauf "
|
||||||
|
f"scheint ausgefallen zu sein."
|
||||||
|
)
|
||||||
|
|
||||||
|
if os.path.exists(MODEL_FILE):
|
||||||
|
model_date = datetime.fromtimestamp(os.path.getmtime(MODEL_FILE)).date()
|
||||||
|
if model_date < last_draw and (datetime.now().date() - model_date).days > 1:
|
||||||
|
problems.append(
|
||||||
|
f"Modelle sind vom {model_date.isoformat()}, aber die neueste "
|
||||||
|
f"Ziehung ({last_draw.isoformat()}) wurde noch nicht eingelernt."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
problems.append("Modell-Datei trained_models.pkl fehlt komplett.")
|
||||||
|
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
problems = check()
|
||||||
|
if not problems:
|
||||||
|
print("✅ Watchdog: alles aktuell.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("⚠️ Watchdog hat Probleme gefunden:")
|
||||||
|
for p in problems:
|
||||||
|
print(f" - {p}")
|
||||||
|
|
||||||
|
message = "🐶 *LOTTO WATCHDOG - PROBLEM ERKANNT*\n\n" + "\n".join(f"⚠️ {p}" for p in problems)
|
||||||
|
LottoNotifier().send_alert(message)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user