Session 2026-04-21: SEO, Wiki-Anreicherung, Training, Lober
SEO & Crawler:
- robots.txt, llms.txt, sitemap.xml (508 Seiten bei Google)
- SSR-Seiten: /info, /wiki/rassen, /wiki/rasse/{slug}, /knigge
- Open Graph, JSON-LD, Breadcrumbs in index.html
Navigation:
- Training unter "Mein Hund", Wissen collapsible
- Welcome-Seite und Landing-Page auf 5-Gruppen-Struktur
Wiki:
- KI-Anreicherung (Claude API): beschreibung, vorkommen_de, Steckbrief
- "So einen hab ich" / Züchter-Verzeichnis
- Scheduler: 50 Rassen beim Start, 20/Nacht
Training:
- Session-Logging (Erfolgsquote, Stimmung, Zufriedenheit)
- Virtueller KI-Trainer (6h-Cache)
- Trainingskalender (Habit-Tracker)
- Top-Training → automatischer Tagebucheintrag
- Gamification ohne Druck: Badges, Streak, Stats
Fortschritts-Lober:
- Jeden Montag 09:00: Claude schreibt Lob-Text pro Hund
- Push + Karte im Tagebuch
Monitoring:
- 4× täglich Status-Mail mit Scheduler-Status + Wiki-Fortschritt
This commit is contained in:
parent
65d1cf6c7f
commit
180de32e57
22 changed files with 4351 additions and 189 deletions
|
|
@ -19,6 +19,9 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
_scheduler = AsyncIOScheduler(timezone="Europe/Berlin")
|
||||
|
||||
# In-Memory Job-Protokoll: {job_id: {"last_run": datetime, "result": str, "status": "ok"|"error"}}
|
||||
_job_log: dict = {}
|
||||
|
||||
|
||||
def start():
|
||||
_scheduler.add_job(
|
||||
|
|
@ -87,8 +90,41 @@ def start():
|
|||
id="seed_wikidata_startup",
|
||||
replace_existing=True,
|
||||
)
|
||||
# Täglich 02:30 Uhr — KI-Anreicherung für 20 noch nicht angereicherte Rassen
|
||||
_scheduler.add_job(
|
||||
_job_wiki_enrich,
|
||||
CronTrigger(hour=2, minute=30),
|
||||
id="wiki_enrich_nightly",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
# Jeden Montag 09:00 — Wöchentlicher Fortschritts-Lober
|
||||
_scheduler.add_job(
|
||||
_job_weekly_praise,
|
||||
CronTrigger(day_of_week='mon', hour=9, minute=0),
|
||||
id="weekly_praise",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
# 4× täglich Status-Report per Mail (07:00, 13:00, 19:00, 01:00)
|
||||
for _h in [7, 13, 19, 1]:
|
||||
_scheduler.add_job(
|
||||
_job_status_report,
|
||||
CronTrigger(hour=_h, minute=0),
|
||||
id=f"status_report_{_h:02d}",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=1800,
|
||||
)
|
||||
# Einmalig beim Start (nach 90s) — erste 50 Rassen sofort anreichern
|
||||
_scheduler.add_job(
|
||||
_job_wiki_enrich_startup,
|
||||
'date',
|
||||
run_date=datetime.now(tz=_TZ) + timedelta(seconds=90),
|
||||
id="wiki_enrich_startup",
|
||||
replace_existing=True,
|
||||
)
|
||||
_scheduler.start()
|
||||
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed beim Start.")
|
||||
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed beim Start, Wiki-KI-Anreicherung 02:30.")
|
||||
|
||||
|
||||
def stop():
|
||||
|
|
@ -150,6 +186,7 @@ async def _job_health_reminders():
|
|||
logger.info(f"Reminder Push: user={r['user_id']} entry={r['id']} delta={delta}d")
|
||||
|
||||
logger.info(f"Health-Reminder Job fertig — {len(rows)} Einträge, {sent_total} Push gesendet.")
|
||||
_log_job("health_reminders", "ok", f"{len(rows)} Einträge, {sent_total} Push")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -176,6 +213,7 @@ async def _job_poison_archive():
|
|||
AND expires_at < ?
|
||||
""", (now,))
|
||||
count = result.rowcount
|
||||
_log_job("poison_archive", "ok", f"{count} Meldungen archiviert")
|
||||
if count:
|
||||
logger.info(f"Giftköder-Archiv: {count} abgelaufene Meldungen archiviert.")
|
||||
|
||||
|
|
@ -202,6 +240,7 @@ async def _job_weather_alert():
|
|||
thunderstorm = summary["thunderstorm"]
|
||||
|
||||
if max_temp >= 28:
|
||||
_log_job("weather_alert", "ok", f"Hitze-Push: {max_temp:.0f}°C")
|
||||
sent = send_push_to_all({
|
||||
"type": "weather_heat",
|
||||
"title": "☀️ Heißer Asphalt heute",
|
||||
|
|
@ -222,6 +261,7 @@ async def _job_weather_alert():
|
|||
return
|
||||
|
||||
logger.info("Wetter-Alert: Keine Warnung nötig heute.")
|
||||
_log_job("weather_alert", "ok", "Keine Warnung")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -301,6 +341,7 @@ async def _job_milestone_check():
|
|||
created_total += 1
|
||||
|
||||
logger.info(f"Meilenstein-Check fertig — {created_total} Einträge erstellt.")
|
||||
_log_job("milestone_check", "ok", f"{created_total} Meilensteine erstellt")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -348,6 +389,7 @@ async def _job_import_events():
|
|||
logger.warning(f"Event-Import: Fehler beim Speichern von '{ev.get('titel')}': {e}")
|
||||
|
||||
logger.info(f"Event-Import: {imported} neue Events importiert (von {len(events)} geparsten).")
|
||||
_log_job("import_events", "ok", f"{imported} neue von {len(events)} Events")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -390,8 +432,10 @@ async def _job_seed_wikidata_breeds():
|
|||
from scraper.wikipedia_photos import fetch_wikipedia_photos
|
||||
wp_count = await fetch_wikipedia_photos()
|
||||
logger.info(f"Wikipedia photo fetch done: {wp_count} Fotos")
|
||||
_log_job("seed_wikidata_startup", "ok", f"{count} Rassen, {mirrored}+{wp_count} Fotos")
|
||||
except Exception as e:
|
||||
logger.error(f"Wikidata-Seed: Fehler: {e}")
|
||||
_log_job("seed_wikidata_startup", "error", str(e))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -575,6 +619,359 @@ async def _job_prewarm_cities():
|
|||
await _send_progress("abgeschlossen ✓", cities_done, total_cities)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hilfsfunktion: Job-Protokoll aktualisieren
|
||||
# ------------------------------------------------------------------
|
||||
def _log_job(job_id: str, status: str, result: str):
|
||||
_job_log[job_id] = {
|
||||
"last_run": datetime.now(tz=_TZ),
|
||||
"status": status,
|
||||
"result": result,
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: KI-Anreicherung der Rassen-Daten (nächtlich)
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_wiki_enrich():
|
||||
"""Reichert 20 noch nicht angereicherte Rassen mit KI-Daten an."""
|
||||
try:
|
||||
from scraper.breed_enricher import enrich_breeds
|
||||
enriched = await enrich_breeds(limit=20)
|
||||
msg = f"{enriched} Rassen angereichert"
|
||||
logger.info(f"Wiki-KI-Anreicherung (nächtlich): {msg}.")
|
||||
_log_job("wiki_enrich_nightly", "ok", msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Wiki-KI-Anreicherung: Fehler: {e}")
|
||||
_log_job("wiki_enrich_nightly", "error", str(e))
|
||||
|
||||
|
||||
async def _job_wiki_enrich_startup():
|
||||
"""Beim Start: erste 50 Rassen sofort anreichern."""
|
||||
try:
|
||||
from scraper.breed_enricher import enrich_breeds
|
||||
enriched = await enrich_breeds(limit=50)
|
||||
msg = f"{enriched} Rassen angereichert (Startup)"
|
||||
logger.info(f"Wiki-KI-Anreicherung (Startup): {msg}.")
|
||||
_log_job("wiki_enrich_startup", "ok", msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Wiki-KI-Anreicherung (Startup): Fehler: {e}")
|
||||
_log_job("wiki_enrich_startup", "error", str(e))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hilfsfunktion: Lob-Text für einen Hund generieren
|
||||
# ------------------------------------------------------------------
|
||||
async def _generate_praise_for_dog(dog: dict, user_id: int) -> str:
|
||||
"""Generiert einen Lob-Text für einen Hund basierend auf der letzten Woche."""
|
||||
from ki import complete, KIUnavailableError
|
||||
import json as _json
|
||||
from datetime import date, timedelta
|
||||
|
||||
since = (date.today() - timedelta(days=7)).isoformat()
|
||||
name = dog["name"]
|
||||
rasse = dog.get("rasse") or "Hund"
|
||||
|
||||
stats = {}
|
||||
try:
|
||||
with db() as conn:
|
||||
stats["diary"] = conn.execute("SELECT COUNT(*) FROM diary WHERE dog_id=? AND datum>=?", (dog["id"], since)).fetchone()[0]
|
||||
stats["training"] = conn.execute("SELECT COUNT(*) FROM training_sessions WHERE dog_id=? AND datum>=?", (dog["id"], since)).fetchone()[0]
|
||||
stats["top_training"] = conn.execute("SELECT COUNT(*) FROM training_sessions WHERE dog_id=? AND datum>=? AND ist_top=1", (dog["id"], since)).fetchone()[0]
|
||||
stats["health"] = conn.execute("SELECT COUNT(*) FROM health WHERE dog_id=? AND datum>=?", (dog["id"], since)).fetchone()[0]
|
||||
stats["days_active"] = conn.execute(
|
||||
"SELECT COUNT(DISTINCT datum) FROM diary WHERE dog_id=? AND datum>=?", (dog["id"], since)
|
||||
).fetchone()[0]
|
||||
# Wie viele Wochen ist der User dabei?
|
||||
first = conn.execute("SELECT MIN(datum) FROM diary WHERE dog_id=?", (dog["id"],)).fetchone()[0]
|
||||
if first:
|
||||
weeks_total = max(1, (date.today() - date.fromisoformat(first)).days // 7)
|
||||
else:
|
||||
weeks_total = 1
|
||||
stats["weeks_total"] = weeks_total
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Prompt aufbauen
|
||||
aktivitaet_parts = []
|
||||
if stats.get("diary", 0):
|
||||
aktivitaet_parts.append(f"{stats['diary']} Tagebuch-Eintr\u00e4ge")
|
||||
if stats.get("training", 0):
|
||||
t = f"{stats['training']} Trainingseinheiten"
|
||||
if stats.get("top_training", 0):
|
||||
t += f" (davon {stats['top_training']} Top-Training)"
|
||||
aktivitaet_parts.append(t)
|
||||
if stats.get("health", 0):
|
||||
aktivitaet_parts.append(f"{stats['health']} Gesundheitseintr\u00e4ge")
|
||||
|
||||
if not aktivitaet_parts:
|
||||
aktivitaet_text = "Diese Woche war ruhig \u2014 keine erfassten Aktivit\u00e4ten."
|
||||
else:
|
||||
aktivitaet_text = ", ".join(aktivitaet_parts)
|
||||
|
||||
prompt = f"""Du bist ein warmer, wohlwollender Begleiter f\u00fcr Hundebesitzer. Schreibe eine kurze pers\u00f6nliche Lob-Nachricht (2-3 S\u00e4tze) f\u00fcr die vergangene Woche.
|
||||
|
||||
Hund: {name} ({rasse})
|
||||
Letzte 7 Tage: {aktivitaet_text}
|
||||
Dabei seit: {stats.get('weeks_total', 1)} Wochen
|
||||
|
||||
Regeln (unbedingt einhalten):
|
||||
- Nur loben, NIEMALS Ratschl\u00e4ge geben oder auf Fehlendes hinweisen
|
||||
- Sprich \u00fcber den Hund: "{name} hatte eine tolle Woche" \u2014 nicht \u00fcber den Besitzer
|
||||
- Auch bei 0 Aktivit\u00e4ten: positive Formulierung (\u201eAuch ruhige Wochen geh\u00f6ren dazu\u201c)
|
||||
- Maximal 3 kurze S\u00e4tze
|
||||
- Warm, pers\u00f6nlich, keine Floskeln
|
||||
- Kein "Du solltest...", kein "Vergiss nicht...", keine Empfehlungen"""
|
||||
|
||||
try:
|
||||
text = await complete(
|
||||
prompt,
|
||||
system="Du schreibst kurze, warme Lob-Nachrichten f\u00fcr Hundebesitzer. Nur Lob, keine Ratschl\u00e4ge.",
|
||||
max_tokens=150,
|
||||
)
|
||||
return text.strip()
|
||||
except Exception:
|
||||
# Fallback wenn KI nicht verfügbar
|
||||
if aktivitaet_parts:
|
||||
return f"{name} hatte eine aktive Woche \u2014 {aktivitaet_text}. Das ist toll! \U0001f43e"
|
||||
else:
|
||||
return f"Auch ruhige Wochen geh\u00f6ren dazu. {name} wei\u00df, dass du f\u00fcr ihn da bist. \U0001f43e"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: Wöchentlicher Fortschritts-Lober
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_weekly_praise():
|
||||
"""Jeden Montag: Lob-Text f\u00fcr alle aktiven Hunde generieren + Push senden."""
|
||||
from datetime import date
|
||||
import json as _json
|
||||
|
||||
today = date.today()
|
||||
d = today.isocalendar()
|
||||
week_key = f"{d[0]}-W{d[1]:02d}"
|
||||
|
||||
logger.info(f"Weekly Praise Job startet f\u00fcr Woche {week_key}")
|
||||
|
||||
# Alle Hunde laden, für die noch kein Lob diese Woche existiert
|
||||
with db() as conn:
|
||||
dogs = conn.execute("""
|
||||
SELECT d.id, d.name, d.rasse, d.user_id, d.foto_url
|
||||
FROM dogs d
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM weekly_praise wp
|
||||
WHERE wp.dog_id=d.id AND wp.week_key=?
|
||||
)
|
||||
ORDER BY d.id
|
||||
""", (week_key,)).fetchall()
|
||||
|
||||
dogs = [dict(d) for d in dogs]
|
||||
logger.info(f"Weekly Praise: {len(dogs)} Hunde ohne Lob diese Woche.")
|
||||
|
||||
import asyncio
|
||||
generated = 0
|
||||
for dog in dogs:
|
||||
try:
|
||||
praise = await _generate_praise_for_dog(dog, dog["user_id"])
|
||||
with db() as conn:
|
||||
conn.execute("""
|
||||
INSERT OR IGNORE INTO weekly_praise (user_id, dog_id, week_key, praise_text)
|
||||
VALUES (?,?,?,?)
|
||||
""", (dog["user_id"], dog["id"], week_key, praise))
|
||||
|
||||
# Push-Notification — erste 100 Zeichen als Preview
|
||||
preview = praise[:100] + "\u2026" if len(praise) > 100 else praise
|
||||
send_push_to_user(dog["user_id"], {
|
||||
"type": "weekly_praise",
|
||||
"title": f"\U0001f43e R\u00fcckblick f\u00fcr {dog['name']}",
|
||||
"body": preview,
|
||||
"data": {"page": "diary"},
|
||||
"tag": f"weekly-praise-{dog['id']}-{week_key}",
|
||||
})
|
||||
|
||||
generated += 1
|
||||
await asyncio.sleep(2) # Rate limiting für KI
|
||||
except Exception as e:
|
||||
logger.error(f"Weekly Praise: Fehler f\u00fcr Hund {dog['id']}: {e}")
|
||||
|
||||
logger.info(f"Weekly Praise Job fertig \u2014 {generated}/{len(dogs)} Lob-Texte generiert.")
|
||||
_log_job("weekly_praise", "ok", f"{generated} Lob-Texte f\u00fcr KW {d[1]}")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: Status-Report per Mail (4× täglich)
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_status_report():
|
||||
"""Sendet einen HTML-Status-Report an ADMIN_EMAIL."""
|
||||
import os
|
||||
from mailer import send_email
|
||||
|
||||
admin = os.getenv("ADMIN_EMAIL", "")
|
||||
if not admin:
|
||||
logger.info("Status-Report: ADMIN_EMAIL nicht gesetzt, übersprungen.")
|
||||
return
|
||||
|
||||
now_str = datetime.now(tz=_TZ).strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
# --- DB-Metriken abrufen ---
|
||||
metrics = {}
|
||||
try:
|
||||
with db() as conn:
|
||||
# Rassen-Anreicherung
|
||||
metrics["rassen_total"] = conn.execute("SELECT COUNT(*) FROM wiki_rassen").fetchone()[0]
|
||||
metrics["rassen_enriched"] = conn.execute("SELECT COUNT(*) FROM wiki_rassen WHERE ki_enriched=1").fetchone()[0]
|
||||
metrics["rassen_mit_foto"] = conn.execute("SELECT COUNT(*) FROM wiki_rassen WHERE foto_url IS NOT NULL AND foto_url NOT LIKE 'http%'").fetchone()[0]
|
||||
metrics["rassen_mit_desc"] = conn.execute("SELECT COUNT(*) FROM wiki_rassen WHERE beschreibung IS NOT NULL AND beschreibung != ''").fetchone()[0]
|
||||
|
||||
# Züchter
|
||||
try:
|
||||
metrics["zuchter_pending"] = conn.execute("SELECT COUNT(*) FROM wiki_zuchter WHERE verified=0").fetchone()[0]
|
||||
metrics["zuchter_verified"] = conn.execute("SELECT COUNT(*) FROM wiki_zuchter WHERE verified=1").fetchone()[0]
|
||||
except Exception:
|
||||
metrics["zuchter_pending"] = metrics["zuchter_verified"] = 0
|
||||
|
||||
# Community
|
||||
metrics["users"] = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
||||
metrics["dogs"] = conn.execute("SELECT COUNT(*) FROM dogs").fetchone()[0]
|
||||
metrics["diary_entries"] = conn.execute("SELECT COUNT(*) FROM diary").fetchone()[0]
|
||||
metrics["poison_active"] = conn.execute("SELECT COUNT(*) FROM poison WHERE geloest=0").fetchone()[0]
|
||||
metrics["lost_active"] = conn.execute("SELECT COUNT(*) FROM lost WHERE gefunden=0").fetchone()[0]
|
||||
|
||||
# Wiki-Interesse
|
||||
try:
|
||||
metrics["interesse_hat"] = conn.execute("SELECT COUNT(*) FROM wiki_breed_interest WHERE typ='hat'").fetchone()[0]
|
||||
metrics["interesse_will"] = conn.execute("SELECT COUNT(*) FROM wiki_breed_interest WHERE typ='will'").fetchone()[0]
|
||||
except Exception:
|
||||
metrics["interesse_hat"] = metrics["interesse_will"] = 0
|
||||
except Exception as e:
|
||||
logger.error(f"Status-Report: DB-Fehler: {e}")
|
||||
return
|
||||
|
||||
# --- Wiki-Fortschritt berechnen ---
|
||||
total = metrics["rassen_total"] or 1
|
||||
enriched = metrics["rassen_enriched"]
|
||||
pct = round(enriched / total * 100)
|
||||
remaining = total - enriched
|
||||
nights_left = (remaining + 19) // 20 # bei 20/Nacht
|
||||
|
||||
bar_filled = round(pct / 5)
|
||||
progress_bar = "█" * bar_filled + "░" * (20 - bar_filled)
|
||||
|
||||
# --- Job-Log-Tabelle ---
|
||||
job_labels = {
|
||||
"health_reminders": "Gesundheits-Erinnerungen",
|
||||
"poison_archive": "Giftköder-Archiv",
|
||||
"weather_alert": "Wetter-Alert",
|
||||
"milestone_check": "Meilenstein-Check",
|
||||
"import_events": "Event-Import (VDH)",
|
||||
"wiki_enrich_nightly": "Wiki KI-Anreicherung (nächtlich)",
|
||||
"wiki_enrich_startup": "Wiki KI-Anreicherung (Startup)",
|
||||
"seed_breeds_startup": "Rassen-Seed (TheDogAPI)",
|
||||
"seed_wikidata_startup":"Rassen-Seed (Wikidata)",
|
||||
"weekly_praise": "Wöchentlicher Lober (Mo 09:00)",
|
||||
}
|
||||
job_rows_html = ""
|
||||
job_rows_txt = ""
|
||||
for jid, label in job_labels.items():
|
||||
log = _job_log.get(jid)
|
||||
if log:
|
||||
ts = log["last_run"].strftime("%d.%m. %H:%M")
|
||||
status = "✅" if log["status"] == "ok" else "❌"
|
||||
result = log["result"]
|
||||
color = "#16a34a" if log["status"] == "ok" else "#dc2626"
|
||||
job_rows_html += f'<tr><td style="padding:5px 10px;color:#555">{label}</td><td style="padding:5px 10px;font-family:monospace;font-size:12px">{ts}</td><td style="padding:5px 10px;color:{color}">{status} {result}</td></tr>'
|
||||
job_rows_txt += f" {status} {label}: {ts} — {result}\n"
|
||||
else:
|
||||
job_rows_html += f'<tr><td style="padding:5px 10px;color:#555">{label}</td><td style="padding:5px 10px;color:#aaa" colspan="2">— noch nicht gelaufen</td></tr>'
|
||||
job_rows_txt += f" — {label}: noch nicht gelaufen\n"
|
||||
|
||||
html = f"""\
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f5f0ea;margin:0;padding:0">
|
||||
<div style="max-width:600px;margin:24px auto;background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 2px 16px rgba(0,0,0,.08)">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="background:linear-gradient(135deg,#C4843A,#e8a857);padding:24px 28px;color:#fff">
|
||||
<div style="font-size:22px;font-weight:800;margin-bottom:2px">🐾 Ban Yaro — Status-Report</div>
|
||||
<div style="opacity:.88;font-size:13px">{now_str} Uhr</div>
|
||||
</div>
|
||||
|
||||
<!-- Wiki-Fortschritt -->
|
||||
<div style="padding:20px 28px;border-bottom:1px solid #f0e8dc">
|
||||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#C4843A;margin-bottom:10px">Wiki KI-Anreicherung</div>
|
||||
<div style="font-family:monospace;font-size:13px;background:#fdf6ef;border-radius:8px;padding:12px 14px;line-height:1.8">
|
||||
<span style="color:#555">{progress_bar}</span> <strong>{pct}%</strong><br>
|
||||
✅ Angereichert: <strong>{enriched}</strong> / {total}<br>
|
||||
⏳ Verbleibend: <strong>{remaining}</strong> Rassen (~{nights_left} Nächte)<br>
|
||||
📷 Mit lokalem Foto: <strong>{metrics['rassen_mit_foto']}</strong><br>
|
||||
📝 Mit Beschreibung: <strong>{metrics['rassen_mit_desc']}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduler-Status -->
|
||||
<div style="padding:20px 28px;border-bottom:1px solid #f0e8dc">
|
||||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#C4843A;margin-bottom:10px">Scheduler-Jobs</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||||
{job_rows_html}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Community-Metriken -->
|
||||
<div style="padding:20px 28px;border-bottom:1px solid #f0e8dc">
|
||||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#C4843A;margin-bottom:10px">Community</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
{"".join(f'<div style="background:#fdf6ef;border-radius:8px;padding:10px 14px"><div style="font-size:20px;font-weight:800;color:#C4843A">{v}</div><div style="font-size:11px;color:#888">{k}</div></div>' for k,v in [
|
||||
("Nutzer",metrics["users"]),
|
||||
("Hunde",metrics["dogs"]),
|
||||
("Tagebuch-Einträge",metrics["diary_entries"]),
|
||||
("Aktive Giftköder",metrics["poison_active"]),
|
||||
("Vermisste Hunde",metrics["lost_active"]),
|
||||
("'So einen hab ich'",metrics["interesse_hat"]),
|
||||
("'Interessiert mich'",metrics["interesse_will"]),
|
||||
("Züchter (pending)",metrics["zuchter_pending"]),
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="padding:14px 28px;background:#fdf6ef;font-size:11px;color:#aaa;text-align:center">
|
||||
Ban Yaro · banyaro.app · Nächster Report in ~6h
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
plain = f"""Ban Yaro Status-Report — {now_str}
|
||||
|
||||
=== Wiki KI-Anreicherung ===
|
||||
{progress_bar} {pct}%
|
||||
Angereichert: {enriched}/{total}
|
||||
Verbleibend: {remaining} Rassen (~{nights_left} Nächte à 20/Nacht)
|
||||
Mit Foto: {metrics['rassen_mit_foto']}
|
||||
Mit Beschreibung: {metrics['rassen_mit_desc']}
|
||||
|
||||
=== Scheduler-Jobs ===
|
||||
{job_rows_txt}
|
||||
=== Community ===
|
||||
Nutzer: {metrics['users']}
|
||||
Hunde: {metrics['dogs']}
|
||||
Tagebuch-Einträge: {metrics['diary_entries']}
|
||||
Aktive Giftköder: {metrics['poison_active']}
|
||||
Vermisste Hunde: {metrics['lost_active']}
|
||||
'So einen hab ich': {metrics['interesse_hat']}
|
||||
'Interessiert mich': {metrics['interesse_will']}
|
||||
Züchter (pending): {metrics['zuchter_pending']}
|
||||
"""
|
||||
|
||||
try:
|
||||
await send_email(admin, f"Ban Yaro Status {now_str}", html, plain)
|
||||
logger.info(f"Status-Report gesendet an {admin}.")
|
||||
except Exception as e:
|
||||
logger.error(f"Status-Report: Mail-Fehler: {e}")
|
||||
|
||||
|
||||
def _compute_milestone(today: date, bday: date, dog_name: str):
|
||||
"""
|
||||
Gibt (titel, text) zurück wenn heute ein Meilenstein-Tag ist,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue