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:
cbazzaandClaude Sonnet 5 committed 2026-09-12 10:02:04 +02:00
1 parent 427e704f3c
commit 612334dc1a
8 files changed
+178 -9899

No files matched your search

+21 -2
View File
@@ -91,7 +91,7 @@ class LottoAPIUpdater:
return False
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):
return True
@@ -99,7 +99,10 @@ class LottoAPIUpdater:
print("\n💾 Erstelle Backup...")
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)
filename = os.path.basename(self.data_file)
@@ -108,12 +111,28 @@ class LottoAPIUpdater:
shutil.copy2(self.data_file, self.backup_file)
print(f" ✅ Backup: {os.path.basename(self.backup_file)}")
self._prune_old_backups(backup_dir, filename)
return True
except Exception as e:
print(f" ⚠️ Backup-Fehler: {e}")
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]:
"""
Holt Daten von spezifischer API.