diff --git a/backend/database.py b/backend/database.py index 8ef5362..e373f02 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1072,6 +1072,19 @@ def _migrate(conn_factory): pass logger.info("Migration: wiki_rassen Anreicherungs-Felder bereit.") + # Moderation-Logging: resolved_by/at für forum_reports, verified_by/at/reject für wiki_zuchter + for table, col, typedef in [ + ("forum_reports", "resolved_by", "INTEGER"), + ("forum_reports", "resolved_at", "TEXT"), + ("wiki_zuchter", "verified_by", "INTEGER"), + ("wiki_zuchter", "verified_at", "TEXT"), + ("wiki_zuchter", "reject_reason", "TEXT"), + ]: + try: + conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}") + except Exception: + pass + # Wiki: Züchter-Verzeichnis conn.executescript(""" CREATE TABLE IF NOT EXISTS wiki_zuchter ( @@ -1631,3 +1644,16 @@ def _migrate(conn_factory): conn.execute("UPDATE training_exercises SET js_exercise_id=? WHERE id=?", (js_id, row['id'])) conn.execute("CREATE INDEX IF NOT EXISTS idx_te_js_id ON training_exercises(js_exercise_id)") logger.info("Migration: training_exercises.js_exercise_id hinzugefügt, 'Fuß' bereinigt.") + + # Hund des Monats — dauerhafte Gewinner-Tabelle + conn.executescript(""" + CREATE TABLE IF NOT EXISTS hund_des_monats_wins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, + monat TEXT NOT NULL, + stimmen INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(dog_id, monat) + ); + CREATE INDEX IF NOT EXISTS idx_hdm_wins_dog ON hund_des_monats_wins(dog_id); + """) diff --git a/backend/main.py b/backend/main.py index d85556a..6eb99a2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1429,6 +1429,13 @@ async def knigge_page(): # ------------------------------------------------------------------ +# /presse — Presseseite +# ------------------------------------------------------------------ +@app.get("/presse") +async def presse(): + return FileResponse(f"{STATIC_DIR}/presse.html", headers={"Cache-Control": "max-age=3600"}) + + # /partner — Influencer-Landingpage # ------------------------------------------------------------------ @app.get("/partner") diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 09a4127..cd3fee1 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -97,6 +97,40 @@ class ThreadAdminPatch(BaseModel): is_deleted: Optional[int] = None +# ------------------------------------------------------------------ +# GET /api/admin/action-items +# ------------------------------------------------------------------ +@router.get("/action-items") +async def action_items(user=Depends(require_mod)): + with db() as conn: + jobs = conn.execute( + "SELECT COUNT(*) FROM job_applications WHERE status IN ('pending','reviewing')" + ).fetchone()[0] + breeders = conn.execute( + "SELECT COUNT(*) FROM users WHERE breeder_status='pending'" + ).fetchone()[0] + reports = conn.execute( + "SELECT COUNT(*) FROM forum_reports WHERE resolved=0" + ).fetchone()[0] + fotos = conn.execute( + "SELECT COUNT(*) FROM wiki_foto_submissions WHERE status='pending'" + ).fetchone()[0] + poi_edits = conn.execute( + "SELECT COUNT(*) FROM osm_poi_edits WHERE status='pending'" + ).fetchone()[0] + users_today = conn.execute( + "SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')" + ).fetchone()[0] + return { + "jobs_pending": jobs, + "breeder_pending": breeders, + "reports_open": reports, + "fotos_pending": fotos, + "poi_edits_pending": poi_edits, + "users_today": users_today, + } + + # ------------------------------------------------------------------ # GET /api/admin/stats # ------------------------------------------------------------------ diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 74f1c95..c7a9066 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -75,6 +75,21 @@ async def list_dogs(user=Depends(get_current_user)): d = dict(r) d["is_guest"] = True result.append(d) + + # HdM-Siege pro Hund anhängen + if result: + dog_ids = [d["id"] for d in result] + with db() as conn: + wins_rows = conn.execute( + f"SELECT dog_id, monat FROM hund_des_monats_wins WHERE dog_id IN ({','.join('?'*len(dog_ids))}) ORDER BY monat DESC", + dog_ids, + ).fetchall() + wins_map: dict[int, list[str]] = {} + for w in wins_rows: + wins_map.setdefault(w["dog_id"], []).append(w["monat"]) + for d in result: + d["hdm_wins"] = wins_map.get(d["id"], []) + return result diff --git a/backend/routes/moderation.py b/backend/routes/moderation.py index 95b33a9..1357a85 100644 --- a/backend/routes/moderation.py +++ b/backend/routes/moderation.py @@ -1,4 +1,5 @@ """BAN YARO — Moderations-Panel Backend""" +from datetime import datetime from fastapi import APIRouter, Depends, HTTPException from database import db from auth import get_current_user @@ -69,17 +70,19 @@ async def mod_stats(user=Depends(require_moderator)): async def mod_reports(user=Depends(require_moderator)): with db() as conn: rows = conn.execute(""" - SELECT r.id, r.target_type, r.target_id, r.grund, r.resolved, r.created_at, - u.name AS melder_name, + SELECT r.id, r.target_type, r.target_id, r.grund, r.resolved, + r.created_at, r.resolved_at, + u.name AS melder_name, + m.name AS resolved_by_name, CASE r.target_type WHEN 'thread' THEN (SELECT t.titel FROM forum_threads t WHERE t.id=r.target_id) WHEN 'post' THEN (SELECT SUBSTR(p.text,1,80) FROM forum_posts p WHERE p.id=r.target_id) END AS content_preview FROM forum_reports r LEFT JOIN users u ON u.id=r.user_id - WHERE r.resolved=0 - ORDER BY r.created_at DESC - LIMIT 100 + LEFT JOIN users m ON m.id=r.resolved_by + ORDER BY r.resolved ASC, r.created_at DESC + LIMIT 200 """).fetchall() return [dict(r) for r in rows] @@ -97,8 +100,12 @@ async def mod_resolve_report(rid: int, user=Depends(require_moderator)): raise HTTPException(404, "Meldung nicht gefunden.") new_state = 0 if r["resolved"] else 1 conn.execute( - "UPDATE forum_reports SET resolved=? WHERE id=?", - (new_state, rid) + """UPDATE forum_reports SET resolved=?, resolved_by=?, resolved_at=? + WHERE id=?""", + (new_state, + user["id"] if new_state else None, + datetime.utcnow().isoformat() if new_state else None, + rid) ) return {"ok": True} @@ -189,17 +196,19 @@ async def mod_patch_user(uid: int, data: dict, user=Depends(require_moderator)): async def mod_fotos(user=Depends(require_moderator)): with db() as conn: rows = conn.execute(""" - SELECT s.id, s.foto_url, s.created_at, + SELECT s.id, s.foto_url, s.status, s.created_at, + s.reviewed_at, s.reject_reason, COALESCE(s.rights_confirmed, 0) AS rights_confirmed, - u.name AS user_name, - r.name AS rasse_name, r.slug AS rasse_slug, + u.name AS user_name, + m.name AS reviewed_by_name, + r.name AS rasse_name, r.slug AS rasse_slug, r.foto_url AS aktuell_foto FROM wiki_foto_submissions s LEFT JOIN users u ON u.id = s.user_id + LEFT JOIN users m ON m.id = s.reviewed_by LEFT JOIN wiki_rassen r ON r.id = s.rasse_id - WHERE s.status = 'pending' - ORDER BY s.created_at ASC - LIMIT 50 + ORDER BY s.status ASC, s.created_at ASC + LIMIT 200 """).fetchall() return [dict(r) for r in rows] @@ -228,11 +237,13 @@ async def mod_poi_edits(user=Depends(require_moderator)): SELECT e.id, e.osm_id, e.poi_name, e.field, e.old_value, e.new_value, e.status, e.created_at, e.resolved_at, - u.name AS einreicher_name + u.name AS einreicher_name, + m.name AS mod_name FROM osm_poi_edits e JOIN users u ON u.id = e.user_id + LEFT JOIN users m ON m.id = e.mod_id ORDER BY e.status ASC, e.created_at DESC - LIMIT 100 + LIMIT 200 """).fetchall() return [dict(r) for r in rows] diff --git a/backend/routes/movies.py b/backend/routes/movies.py index 399c583..da6c682 100644 --- a/backend/routes/movies.py +++ b/backend/routes/movies.py @@ -94,6 +94,71 @@ _SEED_FILME = [ {"id": "white-fang", "titel": "Wolfsblut", "originaltitel": "White Fang", "jahr": 1991, "genre": "Abenteuer/Familie", "typ": "film", "hund_rasse": "Wolf-Hund-Hybrid", "stirbt_der_hund": False, "beschreibung": "Jack Londons Klassiker mit Ethan Hawke: Ein Wolf-Hund-Hybrid findet in Alaska zwischen Wildnis und Menschenwelt seinen Platz.", "bild_emoji": "❄️", "imdb_rating": 6.7}, {"id": "because-of-winn-dixie","titel": "Winn-Dixie", "originaltitel": "Because of Winn-Dixie", "jahr": 2005, "genre": "Familie/Drama", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Ein einsames Mädchen findet einen streunenden Hund im Supermarkt — und mit ihm eine ganze Gemeinschaft.", "bild_emoji": "🛒", "imdb_rating": 6.4}, {"id": "lassie-2005", "titel": "Lassie (2005)", "jahr": 2005, "genre": "Familie/Abenteuer", "typ": "film", "hund_rasse": "Collie", "stirbt_der_hund": False, "beschreibung": "Britische Neuverfilmung mit Peter O'Toole. Lassie flieht aus Schottland und findet den langen Weg nach Yorkshire. Atmosphärisch.", "bild_emoji": "🏴", "imdb_rating": 6.7}, + # ── Neue Einträge: Animation ────────────────────────────────────── + {"id": "all-dogs-go-to-heaven", "titel": "Alle Hunde kommen in den Himmel", "originaltitel": "All Dogs Go to Heaven", "jahr": 1989, "genre": "Animation/Familie", "typ": "film", "hund_rasse": "Schäferhund / Mischling", "stirbt_der_hund": True, "beschreibung": "Don Bluth-Klassiker: Gauner-Hund Charlie entkommt dem Himmel und sucht Rache — bis er sich in ein Waisenmädchen verliebt.", "bild_emoji": "😇", "imdb_rating": 6.8}, + {"id": "all-dogs-go-heaven-2", "titel": "Alle Hunde kommen in den Himmel 2", "originaltitel": "All Dogs Go to Heaven 2", "jahr": 1996, "genre": "Animation/Familie", "typ": "film", "hund_rasse": "Schäferhund / Mischling", "stirbt_der_hund": False, "beschreibung": "Fortsetzung: Charlie kehrt aus dem Himmel zurück nach San Francisco — mit Charlie Sheen als Synchronstimme.", "bild_emoji": "😇", "imdb_rating": 5.4}, + {"id": "oliver-and-company", "titel": "Oliver & Co.", "originaltitel": "Oliver & Company", "jahr": 1988, "genre": "Animation/Familie", "typ": "film", "hund_rasse": "Verschiedene (Dodger = Mischling)", "stirbt_der_hund": False, "beschreibung": "Disney modernisiert Oliver Twist: Ein obdachloser Kater schließt sich einer Hunde-Gang unter Anführer Dodger im New York der 1980er an.", "bild_emoji": "🎸", "imdb_rating": 6.7, "streaming": "Disney+"}, + {"id": "peanuts-movie", "titel": "Die Peanuts — Der Film", "originaltitel": "The Peanuts Movie", "jahr": 2015, "genre": "Animation/Familie", "typ": "film", "hund_rasse": "Beagle", "stirbt_der_hund": False, "beschreibung": "Snoopy und Charlie Brown auf der großen Leinwand. Perfekt für alle, die mit dem Kultcomic aufgewachsen sind.", "bild_emoji": "✈️", "imdb_rating": 7.0, "streaming": "Disney+"}, + {"id": "clifford-big-red-dog", "titel": "Clifford — Der große rote Hund", "originaltitel": "Clifford the Big Red Dog", "jahr": 2021, "genre": "Animation/Familie", "typ": "film", "hund_rasse": "Mischling (riesig)", "stirbt_der_hund": False, "beschreibung": "Der riesige rote Hund aus dem Kinderbuchklassiker kommt ins Kino — Chaos und Herz für die ganze Familie.", "bild_emoji": "🔴", "imdb_rating": 5.4, "streaming": "Amazon Prime"}, + {"id": "101-dalmatians-1996", "titel": "101 Dalmatiner (1996)", "originaltitel": "101 Dalmatians", "jahr": 1996, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Dalmatiner", "stirbt_der_hund": False, "beschreibung": "Glenn Close als unvergessliche Cruella de Vil im Live-Action-Remake des Disney-Klassikers — böse, schrill und absolut unterhaltsam.", "bild_emoji": "🐾", "imdb_rating": 5.7, "streaming": "Disney+"}, + {"id": "102-dalmatians", "titel": "102 Dalmatiner", "originaltitel": "102 Dalmatians", "jahr": 2000, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Dalmatiner", "stirbt_der_hund": False, "beschreibung": "Cruella ist scheinbar geläutert — aber die Dalmatiner-Welpen sind wieder in Gefahr. Fortsetzung mit Glenn Close.", "bild_emoji": "🐾", "imdb_rating": 4.9, "streaming": "Disney+"}, + {"id": "lady-and-tramp-2019", "titel": "Susi und Strolch (2019)", "originaltitel": "Lady and the Tramp", "jahr": 2019, "genre": "Familie/Romanze", "typ": "film", "hund_rasse": "Cocker Spaniel / Mischling", "stirbt_der_hund": False, "beschreibung": "Disney+ Live-Action-Remake mit echten Hunden: Die ikonische Spaghetti-Szene neu inszeniert, herzlich und mit modernem Charme.", "bild_emoji": "🍝", "imdb_rating": 6.4, "streaming": "Disney+"}, + {"id": "my-dog-tulip", "titel": "Mein Hund Tulip", "originaltitel": "My Dog Tulip", "jahr": 2009, "genre": "Animation/Drama", "typ": "film", "hund_rasse": "Deutscher Schäferhund", "stirbt_der_hund": True, "beschreibung": "Animierter Arthouse-Film nach J.R. Ackerley: Ein eigenbrötlerischer Engländer und die vorbehaltlose Liebe zu seiner Schäferhündin Tulip.", "bild_emoji": "🌷", "imdb_rating": 7.0}, + {"id": "bluey", "titel": "Bluey", "jahr": 2018, "genre": "Animation/Kinder", "typ": "serie", "hund_rasse": "Blue Heeler", "stirbt_der_hund": False, "beschreibung": "Australische Kinder-Animationsserie über eine Blue-Heeler-Familie, die weltweit Eltern und Kinder gleichermaßen begeistert. Meistgeliebte Kinderserie des 21. Jahrhunderts.", "bild_emoji": "💙", "imdb_rating": 9.0, "streaming": "Disney+"}, + {"id": "strays-2023", "titel": "Strays — Lass uns Hunde sein", "originaltitel": "Strays", "jahr": 2023, "genre": "Animation/Komödie", "typ": "film", "hund_rasse": "Mischling / Australian Shepherd", "stirbt_der_hund": False, "beschreibung": "Komplett obszöne Erwachsenen-Trickfilm-Komödie: Verlassener Hund will mit neuen Hundefreunden Rache am Herrchen nehmen. Nicht für Kinder!","bild_emoji": "🤬", "imdb_rating": 5.8, "streaming": "Amazon Prime"}, + # ── Neue Einträge: Familie/Drama ────────────────────────────────── + {"id": "hotel-for-dogs", "titel": "Hotel für Hunde", "originaltitel": "Hotel for Dogs", "jahr": 2009, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Zwei Geschwister verwandeln ein leerstehendes Hotel in ein geheimes Paradies für Straßenhunde. Herzerwärmende Familienunterhaltung.", "bild_emoji": "🏨", "imdb_rating": 5.8}, + {"id": "snow-dogs", "titel": "Snowdogs", "originaltitel": "Snow Dogs", "jahr": 2002, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Siberian Husky", "stirbt_der_hund": False, "beschreibung": "Cuba Gooding Jr. erbt eine Schlittenhunde-Meute in Alaska und muss erst lernen, mit ihnen umzugehen. Leichte Disney-Komödie.", "bild_emoji": "🛷", "imdb_rating": 4.2, "streaming": "Disney+"}, + {"id": "a-dogs-way-home", "titel": "Auf dem Heimweg — Lassie und Ich", "originaltitel": "A Dog's Way Home", "jahr": 2019, "genre": "Abenteuer/Familie", "typ": "film", "hund_rasse": "Pit-Bull-Mischling", "stirbt_der_hund": False, "beschreibung": "Hündin Bella verliert sich 400 Meilen von zu Hause entfernt und kämpft sich durch alle Widrigkeiten zurück zu ihrem Herrchen.", "bild_emoji": "🏡", "imdb_rating": 6.7, "streaming": "Netflix"}, + {"id": "fluke", "titel": "Fluke — Das fremde Ich", "originaltitel": "Fluke", "jahr": 1995, "genre": "Drama/Fantasy", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Ein Mann stirbt und wird als Hund wiedergeboren — und kehrt zu seiner Familie zurück. Ungewöhnliches Dramafantasy mit tiefem emotionalem Kern.", "bild_emoji": "🔄", "imdb_rating": 6.2}, + {"id": "zeus-and-roxanne", "titel": "Zeus und Roxanne", "originaltitel": "Zeus and Roxanne", "jahr": 1997, "genre": "Familie/Abenteuer", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Ein Hund und ein Delfin werden beste Freunde — und bringen dabei auch ihre Besitzer zusammen. Charmante Familienunterhaltung der 90er.", "bild_emoji": "🐬", "imdb_rating": 5.2}, + {"id": "benji-2018", "titel": "Benji (2018)", "originaltitel": "Benji", "jahr": 2018, "genre": "Familie/Abenteuer", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Netflix-Remake des Klassikers: Der streunende Hund Benji rettet erneut Kinder aus gefährlichen Händen — jetzt für eine neue Generation.", "bild_emoji": "🐾", "imdb_rating": 6.3, "streaming": "Netflix"}, + {"id": "ugly-dachshund", "titel": "Der hässliche Dackel", "originaltitel": "The Ugly Dachshund", "jahr": 1966, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Deutscher Dogge / Dackel", "stirbt_der_hund": False, "beschreibung": "Disney-Klassiker: Eine Harlekindogge wird von Dackeln aufgezogen und denkt, sie sei selbst ein Dackel. Harmlose Familienkomödie.", "bild_emoji": "😅", "imdb_rating": 6.4}, + {"id": "shiloh", "titel": "Shiloh — Mein treuer Freund", "originaltitel": "Shiloh", "jahr": 1996, "genre": "Familie/Drama", "typ": "film", "hund_rasse": "Beagle", "stirbt_der_hund": False, "beschreibung": "Ein Junge in West Virginia rettet einen misshandelten Beagle vor seinem brutalen Besitzer — eine Geschichte über Mut und Gewissen.", "bild_emoji": "🌿", "imdb_rating": 6.7}, + {"id": "iron-will", "titel": "Iron Will", "jahr": 1994, "genre": "Abenteuer/Familie", "typ": "film", "hund_rasse": "Schlittenhunde", "stirbt_der_hund": False, "beschreibung": "Junger Mann rettet die Farm seiner Familie mit einem gewagten Schlittenhunde-Rennen von Kanada nach Minnesota. Inspirierend und episch.", "bild_emoji": "🏆", "imdb_rating": 6.7, "streaming": "Disney+"}, + {"id": "belle-et-sebastien", "titel": "Belle und Sébastien", "originaltitel": "Belle et Sébastien", "jahr": 2013, "genre": "Familie/Abenteuer", "typ": "film", "hund_rasse": "Pyrenäen-Berghund", "stirbt_der_hund": False, "beschreibung": "Französischer Familienfilm: Ein Waisenjunge und die riesige Berghündin Belle sind beste Freunde in den Alpen des Zweiten Weltkriegs.", "bild_emoji": "🏔️", "imdb_rating": 7.0}, + {"id": "dog-of-flanders", "titel": "Ein Hund von Flandern", "originaltitel": "A Dog of Flanders", "jahr": 1999, "genre": "Drama/Familie", "typ": "film", "hund_rasse": "Bouvier des Flandres", "stirbt_der_hund": True, "beschreibung": "Bewegende Verfilmung des Klassikers: Ein armer Junge in Belgien und sein Hund träumen von Kunst und Würde — mit tragischem Ende.", "bild_emoji": "🎨", "imdb_rating": 7.0}, + {"id": "underdog-2007", "titel": "Underdog — Ein Held auf vier Pfoten", "originaltitel": "Underdog", "jahr": 2007, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Beagle", "stirbt_der_hund": False, "beschreibung": "Ein Laborhund erhält Superkräfte und wird zum maskierten Superhelden der Stadt. Leichte Disney-Familienkomödie nach der Zeichentrickserie.", "bild_emoji": "🦸", "imdb_rating": 5.1}, + {"id": "bingo-1991", "titel": "Bingo", "jahr": 1991, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Ein Zirkushund reist quer durch Amerika, um seinen jungen Herrchen zu finden. Kindheitserinnerung der frühen 90er.", "bild_emoji": "🎪", "imdb_rating": 5.8}, + {"id": "goodbye-my-lady", "titel": "Leb wohl, Lady", "originaltitel": "Goodbye, My Lady", "jahr": 1956, "genre": "Familie/Drama", "typ": "film", "hund_rasse": "Basenji", "stirbt_der_hund": False, "beschreibung": "Ein Junge im Mississippi-Sumpfland findet einen seltsamen lachenden Hund — und muss ihn am Ende zurückgeben. Zeitloser Klassiker.", "bild_emoji": "🌊", "imdb_rating": 7.1}, + {"id": "mitt-liv-som-hund", "titel": "Mein Leben als Hund", "originaltitel": "Mitt liv som hund", "jahr": 1985, "genre": "Drama/Komödie", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": True, "beschreibung": "Lasse Hallströms schwedisches Meisterwerk: Ein Junge wird aufs Land geschickt und vergleicht sein Leben mit dem Schicksal von Laika.", "bild_emoji": "🇸🇪", "imdb_rating": 7.8}, + {"id": "homeward-bound-2", "titel": "Auf dem Weg nach Hause 2 — Im Großstadtdschungel", "originaltitel": "Homeward Bound II: Lost in San Francisco", "jahr": 1996, "genre": "Abenteuer/Familie", "typ": "film", "hund_rasse": "Bullterrier / Golden Retriever", "stirbt_der_hund": False, "beschreibung": "Die Tiere aus Teil 1 verirren sich in San Francisco und müssen erneut den Weg nach Hause finden — diesmal durch die Stadt.", "bild_emoji": "🌉", "imdb_rating": 6.0, "streaming": "Disney+"}, + {"id": "alpha-2018", "titel": "Alpha", "jahr": 2018, "genre": "Abenteuer/Drama", "typ": "film", "hund_rasse": "Wolf", "stirbt_der_hund": False, "beschreibung": "Vor 20.000 Jahren: Ein junger Jäger freundet sich mit einem verletzten Wolf an und legt damit den Grundstein für die Mensch-Hund-Beziehung.", "bild_emoji": "🐺", "imdb_rating": 6.7, "streaming": "Amazon Prime"}, + {"id": "nankyoku-monogatari", "titel": "Antarktis", "originaltitel": "Nankyoku Monogatari", "jahr": 1983, "genre": "Drama/Abenteuer", "typ": "film", "hund_rasse": "Sakhalin Husky", "stirbt_der_hund": True, "beschreibung": "Japanisches Meisterwerk: 15 Schlittenhunde werden 1958 in der Antarktis zurückgelassen — die wahre Geschichte zweier Überlebender.", "bild_emoji": "🇯🇵", "imdb_rating": 7.7}, + # ── Neue Einträge: Komödie ──────────────────────────────────────── + {"id": "cats-and-dogs", "titel": "Cats & Dogs", "jahr": 2001, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Beagle / Verschiedene", "stirbt_der_hund": False, "beschreibung": "Geheimdienstagenten auf vier Pfoten: Hunde gegen Katzen im Kampf um die Weltherrschaft. Spionagefilm-Parodie für die ganze Familie.", "bild_emoji": "🐱", "imdb_rating": 5.2}, + {"id": "cats-and-dogs-2", "titel": "Cats & Dogs 2 — Die Rache der Kitty Kahlohr", "originaltitel": "Cats & Dogs: The Revenge of Kitty Galore", "jahr": 2010, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Fortsetzung: Hunde und Katzen müssen kooperieren, um eine wahnsinnige Superschurkin-Katze zu stoppen.", "bild_emoji": "😾", "imdb_rating": 4.1}, + {"id": "shaggy-dog-1959", "titel": "Der zottige Hund", "originaltitel": "The Shaggy Dog", "jahr": 1959, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Alter Ungarischer Hirtenhund (Sheepdogähnlich)", "stirbt_der_hund": False, "beschreibung": "Disney-Familienklassiker: Ein Teenager verwandelt sich durch einen magischen Ring immer wieder in einen Hund. Mit Fred MacMurray.", "bild_emoji": "✨", "imdb_rating": 6.3}, + {"id": "shaggy-dog-2006", "titel": "The Shaggy Dog (2006)", "originaltitel": "The Shaggy Dog", "jahr": 2006, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Bobtail (Alter Englischer Schäferhund)", "stirbt_der_hund": False, "beschreibung": "Tim Allen als Staatsanwalt, der sich in einen Hund verwandelt — modernes Remake des Disney-Klassikers mit Slapstick-Humor.", "bild_emoji": "✨", "imdb_rating": 5.2, "streaming": "Disney+"}, + {"id": "marmaduke-2010", "titel": "Marmaduke", "jahr": 2010, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Deutsche Dogge", "stirbt_der_hund": False, "beschreibung": "Die riesige Comic-Dogge Marmaduke zieht nach Kalifornien und mischt die dortige Hunde-Gesellschaft auf. Lockerleichte Familienkomödie.", "bild_emoji": "😬", "imdb_rating": 4.6}, + {"id": "show-dogs", "titel": "Show Dogs", "jahr": 2018, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Rottweiler", "stirbt_der_hund": False, "beschreibung": "Ein Polizei-Rottweiler muss undercover bei einer Hundeschau ermitteln. Kindliche Spionagekomödie mit Will Arnett.", "bild_emoji": "🏅", "imdb_rating": 4.3}, + {"id": "must-love-dogs", "titel": "Must Love Dogs", "jahr": 2005, "genre": "Romanze/Komödie", "typ": "film", "hund_rasse": "Neufundländer", "stirbt_der_hund": False, "beschreibung": "Romantische Komödie: Eine frisch Geschiedene sucht online die Liebe — und ein Hund spielt dabei die entscheidende Rolle. Mit Diane Lane.", "bild_emoji": "💕", "imdb_rating": 6.0}, + {"id": "best-in-show", "titel": "Best in Show", "jahr": 2000, "genre": "Komödie/Mockumentary", "typ": "film", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Christopher Guest's genialer Mockumentary über völlig überdrehte Hundeshow-Besucher beim Mayflower Dog Show — vernichtende Satire auf Hundebesitzer.", "bild_emoji": "🎭", "imdb_rating": 7.8}, + {"id": "wiener-dog", "titel": "Wiener-Dog", "jahr": 2016, "genre": "Drama/Komödie", "typ": "film", "hund_rasse": "Dackel", "stirbt_der_hund": True, "beschreibung": "Todd Solondz' episodischer Indie-Film: Ein kleiner Dackel wandert durch mehrere skurrile Menschenleben. Dunkel, philosophisch, preisgekrönt.", "bild_emoji": "🌭", "imdb_rating": 6.6}, + # ── Neue Einträge: Action/Thriller ─────────────────────────────── + {"id": "mans-best-friend-1993", "titel": "Man's Best Friend", "originaltitel": "Man's Best Friend", "jahr": 1993, "genre": "Horror/Thriller", "typ": "film", "hund_rasse": "Mastiff", "stirbt_der_hund": True, "beschreibung": "Ein genetisch manipulierter Kettenhund bricht aus einem Labor aus und entpuppt sich als tödliche Bedrohung. B-Movie-Horrorklassiker.", "bild_emoji": "🧬", "imdb_rating": 5.1}, + {"id": "white-dog-1982", "titel": "White Dog", "originaltitel": "White Dog", "jahr": 1982, "genre": "Drama/Thriller", "typ": "film", "hund_rasse": "Weißer Schäferhund", "stirbt_der_hund": False, "beschreibung": "Samuel Fullers politisch brisanter Film: Ein weißer Schäferhund wurde auf schwarze Menschen abgerichtet — und soll nun umtrainiert werden.", "bild_emoji": "⚪", "imdb_rating": 7.2}, + {"id": "hound-of-baskervilles", "titel": "Der Hund von Baskerville", "originaltitel": "The Hound of the Baskervilles", "jahr": 1939, "genre": "Krimi/Thriller", "typ": "film", "hund_rasse": "Fantastisches Wesen", "stirbt_der_hund": False, "beschreibung": "Basil Rathbone als Sherlock Holmes in der besten Verfilmung des Doyle-Klassikers — die unheilvolle Legende des Dartmoor-Hundes.", "bild_emoji": "🔦", "imdb_rating": 7.4}, + # ── Neue Einträge: Japan/International ─────────────────────────── + {"id": "mari-to-koinu", "titel": "Mari und ihr Hundewelpe", "originaltitel": "Mari to koinu no monogatari", "jahr": 2007, "genre": "Drama/Familie", "typ": "film", "hund_rasse": "Shiba Inu", "stirbt_der_hund": False, "beschreibung": "Wahre Geschichte: Eine Shiba-Inu-Mutter rettet nach einem Erdbeben in den japanischen Alpen ihr Herrchen und ihre Welpen.", "bild_emoji": "🏔️", "imdb_rating": 7.2}, + {"id": "ginga-nagareboshi-gin", "titel": "Ginga: Nagareboshi Gin", "originaltitel": "Ginga: Nagareboshi Gin", "jahr": 1986, "genre": "Anime/Abenteuer", "typ": "serie", "hund_rasse": "Akita Inu", "stirbt_der_hund": False, "beschreibung": "Legendärer japanischer Anime: Silber, ein junger Akita, kämpft gegen einen gigantischen Bären — packende Shonen-Klassikerserie der 80er.", "bild_emoji": "⭐", "imdb_rating": 8.0}, + # ── Neue Einträge: Serien ────────────────────────────────────────── + {"id": "dog-whisperer", "titel": "Der Hundeflüsterer", "originaltitel": "Dog Whisperer with Cesar Millan", "jahr": 2004, "genre": "Dokumentation/Reality", "typ": "serie", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Cesar Millan rehabilitiert Problemhunde und schult deren Besitzer. Legendäre Reality-Serie, die das Hundetraining nachhaltig beeinflusst hat.", "bild_emoji": "🤫", "imdb_rating": 7.8}, + {"id": "its-me-or-the-dog", "titel": "Ich oder der Hund", "originaltitel": "It's Me or the Dog", "jahr": 2005, "genre": "Dokumentation/Reality","typ": "serie", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Victoria Stilwell bringt unerzogenen Hunden Manieren bei — mit Positiver Verstärkung statt Dominanztheorie. Britische Erfolgsrealityshow.", "bild_emoji": "💪", "imdb_rating": 7.2}, + {"id": "lucky-dog", "titel": "Lucky Dog", "originaltitel": "Lucky Dog", "jahr": 2013, "genre": "Dokumentation/Reality","typ": "serie", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Trainer Brandon McMillan rettet Todestrakt-Hunde aus Tierheimen und trainiert sie innerhalb einer Woche als perfekte Familienhunde.", "bild_emoji": "🌟", "imdb_rating": 7.5}, + {"id": "dog-impossible", "titel": "Dog: Impossible", "originaltitel": "Dog: Impossible", "jahr": 2019, "genre": "Dokumentation/Reality","typ": "serie", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Matt Beisner arbeitet mit aggressiven und traumatisierten Hunden, die andere aufgegeben haben. National Geographic's bewegende Trainerserie.", "bild_emoji": "🙏", "imdb_rating": 8.1}, + {"id": "belle-sebastian-anime", "titel": "Belle und Sebastian (Anime)", "originaltitel": "Belle et Sébastien", "jahr": 1984, "genre": "Anime/Familie", "typ": "serie", "hund_rasse": "Pyrenäen-Berghund", "stirbt_der_hund": False, "beschreibung": "Japanische Zeichentrickserie der NHK: Sébastien und sein riesiger weißer Hund Belle in den Alpen — eine Lieblingsserie mehrerer Generationen.", "bild_emoji": "⛰️", "imdb_rating": 7.5}, + {"id": "the-dog-house", "titel": "The Dog House", "originaltitel": "The Dog House", "jahr": 2019, "genre": "Dokumentation", "typ": "serie", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Channel 4-Doku über Wood Green Animal Shelter: Zuschauer beobachten, wie Hunde und Menschen füreinander bestimmt werden. Britische Kultdoku.", "bild_emoji": "🏡", "imdb_rating": 8.2}, + {"id": "puppy-dog-pals", "titel": "Puppy Dog Pals", "originaltitel": "Puppy Dog Pals", "jahr": 2017, "genre": "Animation/Kinder", "typ": "serie", "hund_rasse": "Mops", "stirbt_der_hund": False, "beschreibung": "Disney Junior-Serie: Zwei Möpse erleben täglich Abenteuer in der Nachbarschaft, während ihr Herrchen weg ist. Ideal für Kleinkinder.", "bild_emoji": "🐾", "imdb_rating": 7.4, "streaming": "Disney+"}, + {"id": "dogs-of-berlin", "titel": "Dogs of Berlin", "jahr": 2018, "genre": "Krimi/Drama", "typ": "serie", "hund_rasse": "Kampfhund", "stirbt_der_hund": False, "beschreibung": "Netflix Deutschland-Originalserie: Zwei Berliner Ermittler aus verschiedenen Welten jagen einen Mörder — düster, social, brutal ehrlich.", "bild_emoji": "🐕", "imdb_rating": 7.4, "streaming": "Netflix"}, + # ── Neue Einträge: Dokumentationen ──────────────────────────────── + {"id": "the-champions-2015", "titel": "The Champions", "jahr": 2015, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Pit Bull", "stirbt_der_hund": False, "beschreibung": "Bewegende Doku über die Pitbulls von Michael Vick's Dogfighting-Ring — und ihre erstaunliche Rehabilitation durch engagierte Retter.", "bild_emoji": "💪", "imdb_rating": 7.8}, + {"id": "one-nation-under-dog", "titel": "One Nation Under Dog", "originaltitel": "One Nation Under Dog", "jahr": 2012, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "HBO-Dokumentation über die komplexe Beziehung der Amerikaner zu Hunden — von Tierheimen bis Luxus-Hundesalons.", "bild_emoji": "🇺🇸", "imdb_rating": 7.4}, + {"id": "dogs-on-the-inside", "titel": "Dogs on the Inside", "originaltitel": "Dogs on the Inside", "jahr": 2014, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Gefangene in einem Hochsicherheitsgefängnis trainieren Tierheim-Hunde — eine Geschichte über Mitgefühl, Verantwortung und zweite Chancen.", "bild_emoji": "🔒", "imdb_rating": 7.6}, + {"id": "wonderdog-2023", "titel": "Wonderdog", "originaltitel": "Wonderdog", "jahr": 2023, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Netflix-Doku über die außergewöhnlichen Fähigkeiten von Hunden: Was können sie wirklich riechen, hören und fühlen? Wissenschaft trifft Herz.", "bild_emoji": "🔬", "imdb_rating": 7.1, "streaming": "Netflix"}, + {"id": "the-supervet", "titel": "The Supervet", "originaltitel": "The Supervet", "jahr": 2014, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Channel 4-Doku über Noel Fitzpatrick, den visionären Veterinärchirurgen, der unheilbar verletzte Tiere mit Hightech-Prothesen rettet.", "bild_emoji": "🦾", "imdb_rating": 8.7}, + {"id": "off-the-chain", "titel": "Off the Chain", "originaltitel": "Off the Chain", "jahr": 2004, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Pit Bull", "stirbt_der_hund": False, "beschreibung": "Erschütternde Doku über Pit Bulls in Amerika — zwischen Missbrauch, Dogfighting-Kultur und den Menschen, die für sie kämpfen.", "bild_emoji": "⛓️", "imdb_rating": 7.2}, + {"id": "war-dog-soldier", "titel": "War Dog: A Soldier's Best Friend", "originaltitel": "War Dog: A Soldier's Best Friend", "jahr": 2017, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Belgischer Schäferhund (Malinois)", "stirbt_der_hund": False, "beschreibung": "National Geographic-Doku über Militärhunde und die unzerstörbare Bindung zu ihren Hundeführern — von der Ausbildung bis zum Einsatz.", "bild_emoji": "🎖️", "imdb_rating": 7.8}, ] _SEED_PROMIS = [ @@ -111,27 +176,25 @@ _SEED_PROMIS = [ def seed_movies(): - """Füllt die movies-Tabelle beim ersten Start (idempotent per INSERT OR IGNORE).""" + """Füllt die movies-Tabelle mit allen Seed-Einträgen (idempotent per INSERT OR IGNORE).""" import logging logger = logging.getLogger(__name__) with db() as conn: - count = conn.execute("SELECT COUNT(*) FROM movies").fetchone()[0] - if count == 0: - for i, f in enumerate(_SEED_FILME): - conn.execute(""" - INSERT OR IGNORE INTO movies - (id, titel, originaltitel, jahr, genre, typ, hund_rasse, - stirbt_der_hund, beschreibung, bild_emoji, imdb_rating, - streaming, sort_order) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) - """, ( - f["id"], f["titel"], f.get("originaltitel"), - f.get("jahr"), f.get("genre"), f.get("typ", "film"), - f.get("hund_rasse"), 1 if f.get("stirbt_der_hund") else 0, - f.get("beschreibung"), f.get("bild_emoji", "🐾"), - f.get("imdb_rating"), f.get("streaming"), i, - )) - logger.info(f"movies: {len(_SEED_FILME)} Filme geseedet.") + for i, f in enumerate(_SEED_FILME): + conn.execute(""" + INSERT OR IGNORE INTO movies + (id, titel, originaltitel, jahr, genre, typ, hund_rasse, + stirbt_der_hund, beschreibung, bild_emoji, imdb_rating, + streaming, sort_order) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + """, ( + f["id"], f["titel"], f.get("originaltitel"), + f.get("jahr"), f.get("genre"), f.get("typ", "film"), + f.get("hund_rasse"), 1 if f.get("stirbt_der_hund") else 0, + f.get("beschreibung"), f.get("bild_emoji", "🐾"), + f.get("imdb_rating"), f.get("streaming"), i, + )) + logger.info(f"movies: seed_movies() ausgeführt, {len(_SEED_FILME)} Einträge in der Liste.") # ------------------------------------------------------------------ @@ -180,7 +243,7 @@ _SORT_COLS = { "jahr_asc": "m.jahr ASC", "imdb": "m.imdb_rating DESC", "bewertung": "community_avg DESC", - "default": "m.sort_order ASC, m.jahr DESC", + "default": "CASE WHEN m.imdb_rating IS NULL THEN 1 ELSE 0 END, m.imdb_rating DESC, m.jahr DESC", } @router.get("/filme") @@ -323,6 +386,34 @@ async def get_hund_des_monats(user=Depends(get_current_user_optional)): return {"monat": monat, "top": [dict(r) for r in rows], "user_vote": user_vote} +@router.get("/hund-des-monats/kandidaten") +async def get_hdm_kandidaten(user=Depends(get_current_user)): + """Alle öffentlichen Hunde anderer User, mit aktuellem Stimmenstand.""" + monat = datetime.now().strftime("%Y-%m") + with db() as conn: + rows = conn.execute(""" + SELECT d.id, d.name, d.rasse, d.foto_url, + u.name AS besitzer_name, + COALESCE(v.stimmen, 0) AS stimmen + FROM dogs d + JOIN users u ON u.id = d.user_id + LEFT JOIN ( + SELECT dog_id, COUNT(*) AS stimmen + FROM hund_des_monats_votes + WHERE monat = ? + GROUP BY dog_id + ) v ON v.dog_id = d.id + WHERE d.is_public = 1 + AND d.user_id != ? + ORDER BY + CASE WHEN d.foto_url IS NOT NULL THEN 0 ELSE 1 END, + stimmen DESC, + d.name ASC + LIMIT 60 + """, (monat, user["id"])).fetchall() + return [dict(r) for r in rows] + + @router.post("/hund-des-monats/vote") async def vote_hund_des_monats(data: HundDesMonatsVoteRequest, user=Depends(get_current_user)): monat = datetime.now().strftime("%Y-%m") @@ -330,7 +421,9 @@ async def vote_hund_des_monats(data: HundDesMonatsVoteRequest, user=Depends(get_ dog = conn.execute("SELECT id, user_id, is_public FROM dogs WHERE id=?", (data.dog_id,)).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") - if dog["user_id"] != user["id"] and not dog["is_public"]: + if dog["user_id"] == user["id"]: + raise HTTPException(403, "Du kannst nicht für deinen eigenen Hund abstimmen.") + if not dog["is_public"]: raise HTTPException(403, "Dieser Hund ist nicht öffentlich.") conn.execute(""" INSERT INTO hund_des_monats_votes (user_id, dog_id, monat) diff --git a/backend/routes/outreach.py b/backend/routes/outreach.py index 85eb624..4fbd03c 100644 --- a/backend/routes/outreach.py +++ b/backend/routes/outreach.py @@ -6,7 +6,7 @@ import smtplib import ssl from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart -from email.utils import formataddr +from email.utils import formataddr, formatdate from datetime import datetime from typing import List, Optional @@ -87,6 +87,7 @@ def _imap_save_sent(msg_bytes: bytes, account: str): def _build_message(to: str, subject: str, body: str, account: str, html: str = None) -> MIMEMultipart: acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"] msg = MIMEMultipart("alternative") + msg["Date"] = formatdate(localtime=False) # UTC explizit, Container hat keine lokale TZ msg["Subject"] = subject msg["From"] = formataddr((acc["name"], acc["from"])) msg["To"] = to @@ -97,11 +98,22 @@ def _build_message(to: str, subject: str, body: str, account: str, html: str = N return msg +_LEGAL_FOOTER = ( + "\n\n---\n" + "Ban Yaro | René Degelmann | Ringstr. 26, D-85560 Ebersberg\n" + "Web: https://banyaro.app | Mail: partner@banyaro.app\n\n" + "Datenschutzhinweis: Deine Kontaktdaten stammen aus deinem öffentlichen Profil. " + "Verarbeitung auf Basis berechtigten Interesses (Art. 6 Abs. 1 lit. f DSGVO). " + "Datenschutzerklärung: https://banyaro.app/datenschutz\n" + "Widerspruch/Löschung: Einfach auf diese Mail antworten." +) + + def _send_smtp(to: str, subject: str, body: str, account: str = "partner", html: str = None): acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"] if not acc["user"] or not acc["pass"]: raise RuntimeError(f"SMTP-Account '{account}' nicht konfiguriert.") - msg = _build_message(to, subject, body, account, html=html) + msg = _build_message(to, subject, body + _LEGAL_FOOTER, account, html=html) msg_bytes = msg.as_bytes() ctx = ssl.create_default_context() with smtplib.SMTP(_SMTP_HOST, _SMTP_PORT, timeout=15) as s: @@ -255,7 +267,7 @@ def send_support_mail(to: str, subject: str, body: str): def outreach_log_endpoint(user=Depends(require_admin)): with db() as conn: rows = conn.execute( - """SELECT ol.id, ol.recipient, ol.subject, ol.sent_at, + """SELECT ol.id, ol.recipient, ol.subject, ol.body, ol.sent_at, ol.from_account, u.name AS sent_by_name FROM outreach_log ol JOIN users u ON u.id = ol.sent_by diff --git a/backend/routes/wiki.py b/backend/routes/wiki.py index 83093d7..bf3c19c 100644 --- a/backend/routes/wiki.py +++ b/backend/routes/wiki.py @@ -694,11 +694,12 @@ async def list_zuchter_pending(user=Depends(get_current_user)): raise HTTPException(403, "Nur Moderatoren.") with db() as conn: rows = conn.execute( - """SELECT z.*, u.name AS user_name + """SELECT z.*, u.name AS user_name, m.name AS verified_by_name FROM wiki_zuchter z LEFT JOIN users u ON u.id = z.user_id - WHERE z.verified=0 - ORDER BY z.created_at ASC""", + LEFT JOIN users m ON m.id = z.verified_by + ORDER BY z.verified ASC, z.created_at ASC + LIMIT 200""", ).fetchall() return [dict(r) for r in rows] @@ -716,8 +717,10 @@ async def verify_zuchter(zuchter_id: int, user=Depends(get_current_user)): ).fetchone() if not row: raise HTTPException(404, "Züchter nicht gefunden.") + from datetime import datetime conn.execute( - "UPDATE wiki_zuchter SET verified=1 WHERE id=?", (zuchter_id,) + "UPDATE wiki_zuchter SET verified=1, verified_by=?, verified_at=? WHERE id=?", + (user["id"], datetime.utcnow().isoformat(), zuchter_id) ) result = conn.execute( "SELECT * FROM wiki_zuchter WHERE id=?", (zuchter_id,) diff --git a/backend/scheduler.py b/backend/scheduler.py index d87ef3f..68a4c07 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -100,6 +100,14 @@ def start(): replace_existing=True, misfire_grace_time=1800, ) + # Täglich 12:00 — Moderation-Overdue-Check + _scheduler.add_job( + _job_moderation_overdue, + CronTrigger(hour=12, minute=0), + id="moderation_overdue", + replace_existing=True, + misfire_grace_time=1800, + ) # 1. Feb / Mai / Aug / Nov 07:00 — Quartalsbericht _scheduler.add_job( _job_quarterly_report, @@ -116,8 +124,16 @@ def start(): replace_existing=True, misfire_grace_time=3600, ) + # 1. des Monats 00:05 — Hund des Monats Sieger festlegen + _scheduler.add_job( + _job_hdm_winner, + CronTrigger(day=1, hour=0, minute=5), + id="hdm_winner", + replace_existing=True, + misfire_grace_time=3600, + ) _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 monatlich 1. des Monats, Status-Report täglich 06:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00. OSM-Cache: on-demand (kein Prewarm).") + 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 monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00. OSM-Cache: on-demand (kein Prewarm).") def stop(): @@ -650,6 +666,115 @@ async def _job_ki_health_report(): # ------------------------------------------------------------------ +async def _job_moderation_overdue(): + """Sendet Alarm-Mail wenn Moderations-Einträge seit >24h offen sind.""" + import os + from mailer import send_email + + admin = os.getenv("ADMIN_EMAIL", "") + if not admin: + return + + SLA_H = 24 + threshold = f"datetime('now', '-{SLA_H} hours')" + + overdue = {} + try: + with db() as conn: + n = conn.execute(f"SELECT COUNT(*) FROM job_applications WHERE status IN ('pending','reviewing') AND created_at < {threshold}").fetchone()[0] + if n: overdue["Bewerbungen"] = n + n = conn.execute(f"SELECT COUNT(*) FROM users WHERE breeder_status='pending' AND created_at < {threshold}").fetchone()[0] + if n: overdue["Züchter-Anträge"] = n + n = conn.execute(f"SELECT COUNT(*) FROM forum_reports WHERE resolved=0 AND created_at < {threshold}").fetchone()[0] + if n: overdue["Forum-Meldungen"] = n + n = conn.execute(f"SELECT COUNT(*) FROM wiki_foto_submissions WHERE status='pending' AND created_at < {threshold}").fetchone()[0] + if n: overdue["Foto-Einreichungen"] = n + n = conn.execute(f"SELECT COUNT(*) FROM osm_poi_edits WHERE status='pending' AND created_at < {threshold}").fetchone()[0] + if n: overdue["POI-Korrekturen"] = n + n = conn.execute(f"SELECT COUNT(*) FROM wiki_zuchter WHERE verified=0 AND created_at < {threshold}").fetchone()[0] + if n: overdue["Züchter-Einreichungen (Wiki)"] = n + except Exception as e: + logger.error(f"Moderation-Overdue-Check: DB-Fehler: {e}") + return + + if not overdue: + logger.info("Moderation-Overdue-Check: Alles im SLA.") + return + + now_str = datetime.now(tz=_TZ).strftime("%d.%m.%Y %H:%M") + rows_html = "".join( + f'