diff --git a/backend/database.py b/backend/database.py index cb4264e..3acd66a 100644 --- a/backend/database.py +++ b/backend/database.py @@ -530,8 +530,6 @@ def _migrate(conn_factory): ("social_content", "exercise_id", "TEXT"), ("social_content", "post_url", "TEXT"), ("dogs", "rasse_id", "INTEGER"), - # Pflege: Schere vs. Trimmen unterscheiden - ("pflege_tipps", "fell_pflege_art", "TEXT"), ] with conn_factory() as conn: for table, column, col_type in migrations: @@ -1110,22 +1108,3 @@ def _migrate(conn_factory): except Exception: pass logger.info("Migration: routes.is_valid bereit.") - - # ki_daily_calls: source-Spalte + PK auf (user_id, date, source) erweitern - ki_cols = {r[1] for r in conn.execute("PRAGMA table_info(ki_daily_calls)").fetchall()} - if "source" not in ki_cols: - conn.executescript(""" - ALTER TABLE ki_daily_calls RENAME TO ki_daily_calls_old; - CREATE TABLE ki_daily_calls ( - user_id INTEGER NOT NULL, - date TEXT NOT NULL, - count INTEGER NOT NULL DEFAULT 0, - source TEXT NOT NULL DEFAULT 'cloud', - PRIMARY KEY (user_id, date, source) - ); - INSERT INTO ki_daily_calls (user_id, date, count, source) - SELECT user_id, date, count, 'cloud' FROM ki_daily_calls_old; - DROP TABLE ki_daily_calls_old; - CREATE INDEX IF NOT EXISTS idx_ki_daily_source ON ki_daily_calls(date, source); - """) - logger.info("Migration: ki_daily_calls.source bereit.") diff --git a/backend/ki.py b/backend/ki.py index b2224f9..dd3b426 100644 --- a/backend/ki.py +++ b/backend/ki.py @@ -25,7 +25,7 @@ logger = logging.getLogger(__name__) KI_MODE = os.getenv("KI_MODE", "local") # off | local | cloud LOCAL_BASE_URL = os.getenv("KI_LOCAL_URL", "http://10.47.11.70:11435/v1") LOCAL_MODEL = os.getenv("KI_LOCAL_MODEL", "gemma-4-31b-it") -CLOUD_MODEL = os.getenv("KI_CLOUD_MODEL", "claude-sonnet-4-6") +CLOUD_MODEL = os.getenv("KI_CLOUD_MODEL", "claude-opus-4-6") ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "") # Lazy Imports — nur laden wenn wirklich benötigt @@ -70,7 +70,6 @@ async def complete( user_is_premium: bool = False, json_mode: bool = False, return_model: bool = False, - return_source: bool = False, ) -> str: """ KI-Completion. Wählt automatisch den richtigen Backend. @@ -82,10 +81,9 @@ async def complete( requires_premium: True = nur für Premium-User (nutzt Cloud) user_is_premium: Ob der anfragende User Premium hat json_mode: Antwort als JSON anfordern - return_source: Falls True: gibt (text, source) zurück, source = 'cloud'|'local' Returns: - KI-Antwort als String, oder (str, str) wenn return_source=True + KI-Antwort als String Raises: KIPremiumRequired: Cloud-Feature ohne Premium @@ -103,26 +101,20 @@ async def complete( # Cloud-Aufruf: nur wenn Premium UND cloud-Modus if requires_premium and user_is_premium and KI_MODE == "cloud": text = await _cloud_complete(prompt, system, max_tokens, json_mode) - if return_model: - return (text, CLOUD_MODEL) - return (text, "cloud") if return_source else text + return (text, CLOUD_MODEL) if return_model else text # Lokaler Aufruf: Entwicklung + Free-User if KI_MODE in ("local", "cloud"): try: text = await _local_complete(prompt, system, max_tokens, json_mode) - if return_model: - return (text, LOCAL_MODEL) - return (text, "local") if return_source else text + return (text, LOCAL_MODEL) if return_model else text except Exception as e: logger.warning(f"Lokales KI-Modell nicht erreichbar: {e}") # Cloud-Fallback: im cloud-Modus immer, sonst nur für Premium-User if ANTHROPIC_KEY and (KI_MODE == "cloud" or (requires_premium and user_is_premium)): logger.info("Fallback auf Cloud-KI.") text = await _cloud_complete(prompt, system, max_tokens, json_mode) - if return_model: - return (text, CLOUD_MODEL) - return (text, "cloud") if return_source else text + return (text, CLOUD_MODEL) if return_model else text raise KIUnavailableError( "KI-Modell momentan nicht erreichbar. Bitte später erneut versuchen." ) from e diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 38e6d1c..0f14a55 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -160,30 +160,8 @@ async def stats(user=Depends(require_mod)): ki_users_today = conn.execute( "SELECT COUNT(DISTINCT user_id) FROM ki_daily_calls WHERE date=DATE('now')" ).fetchone()[0] - # Aufschlüsselung nach Quelle (heute) - _src_today = { - r[0]: r[1] for r in conn.execute( - "SELECT source, COALESCE(SUM(count),0) FROM ki_daily_calls " - "WHERE date=DATE('now') GROUP BY source" - ).fetchall() - } - ki_cloud_today = _src_today.get("cloud", 0) - ki_local_today = _src_today.get("local", 0) - ki_luna_today = _src_today.get("luna", 0) - # Aufschlüsselung nach Quelle (Monat) - _src_month = { - r[0]: r[1] for r in conn.execute( - "SELECT source, COALESCE(SUM(count),0) FROM ki_daily_calls " - "WHERE date>=DATE('now','start of month') GROUP BY source" - ).fetchall() - } - ki_cloud_month = _src_month.get("cloud", 0) - ki_local_month = _src_month.get("local", 0) - ki_luna_month = _src_month.get("luna", 0) except Exception: ki_today = ki_month = ki_users_today = 0 - ki_cloud_today = ki_local_today = ki_luna_today = 0 - ki_cloud_month = ki_local_month = ki_luna_month = 0 # Social Media Tracking try: @@ -239,12 +217,6 @@ async def stats(user=Depends(require_mod)): "ki_today": ki_today, "ki_month": ki_month, "ki_users_today": ki_users_today, - "ki_cloud_today": ki_cloud_today, - "ki_local_today": ki_local_today, - "ki_luna_today": ki_luna_today, - "ki_cloud_month": ki_cloud_month, - "ki_local_month": ki_local_month, - "ki_luna_month": ki_luna_month, "social_total": social_total, "social_published": social_published, "social_scheduled": social_scheduled, diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 0b09ae2..74b6d5b 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -361,9 +361,8 @@ async def get_pflege_tipps(dog_id: int, user=Depends(get_current_user)): (f"%{dog['rasse']}%",) ).fetchone() - # Fell-Typ und Pflegeart ableiten + # Fell-Typ ableiten fell_filter = None - fell_pflege_art_filter = None if rasse_info: beschr = (rasse_info["beschreibung"] or "").lower() if any(w in beschr for w in ["lockig", "wellig", "kraus", "pudel", "doodle"]): @@ -375,12 +374,6 @@ async def get_pflege_tipps(dog_id: int, user=Depends(get_current_user)): elif rasse_info["groesse"] in ("gross", "sehr_gross"): fell_filter = "doppel" - # Pflegeart: Trimmen vs. Schneiden - if any(w in beschr for w in ["trimm", "hand-stripping", "stripping", "rauhhaar", "drahthaar", "rauhaar"]): - fell_pflege_art_filter = "trimmen" - elif any(w in beschr for w in ["schneid", "geschoren", "schere", "clipper"]): - fell_pflege_art_filter = "schneiden" - with db() as conn: alle_tipps = conn.execute( "SELECT * FROM pflege_tipps ORDER BY kategorie, titel" @@ -395,15 +388,10 @@ async def get_pflege_tipps(dog_id: int, user=Depends(get_current_user)): result = [] for t in alle_tipps: t = dict(t) - # Fell-Typ-Filter + # Fell-Filter if fell_filter and t["fell_typ"] and t["fell_typ"] != "alle": if fell_filter not in t["fell_typ"].split(","): continue - # Pflegeart-Filter: Trimm-Tipps nicht bei Schneidehunden und umgekehrt - tipp_art = t.get("fell_pflege_art") - if tipp_art and tipp_art != "alle" and fell_pflege_art_filter: - if tipp_art != fell_pflege_art_filter: - continue t["schritte"] = _json.loads(t["schritte"] or "[]") t["saisonal_aktuell"] = bool(t["saison"] and heute_saison in t["saison"]) result.append(t) @@ -415,10 +403,9 @@ async def get_pflege_tipps(dog_id: int, user=Depends(get_current_user)): tipp_des_tages = (saisonal or result)[day_hash % len(saisonal or result)] if result else None return { - "dog_name": dog["name"], - "rasse_name": rasse_info["name"] if rasse_info else dog["rasse"], - "tipp_des_tages": tipp_des_tages, - "tipps": result, - "kategorien": list(dict.fromkeys(t["kategorie"] for t in result)), - "fell_pflege_art": fell_pflege_art_filter, # 'schneiden' | 'trimmen' | None + "dog_name": dog["name"], + "rasse_name": rasse_info["name"] if rasse_info else dog["rasse"], + "tipp_des_tages": tipp_des_tages, + "tipps": result, + "kategorien": list(dict.fromkeys(t["kategorie"] for t in result)), } diff --git a/backend/routes/social.py b/backend/routes/social.py index f0f4d30..53f7901 100644 --- a/backend/routes/social.py +++ b/backend/routes/social.py @@ -466,19 +466,9 @@ _PFLEGE_TIPPS = [ "fell_typ":"lang","saison":None,"tipp":"Von Beinen und Schwanz beginnen — dort filzt es zuerst."}, {"id":"fell_buersten_lockig","titel":"Pflege bei Lockenfell (Pudel, Labradoodle)","kat":"Fell", "beschreibung":"Lockiges Fell verliert kaum Haare, verfilzt aber stark — spezielle Technik nötig.", - "schritte":["Täglich durchkämmen mit Metallkamm","Verfilzungen mit Finger lösen, dann Kamm","Alle 6–8 Wochen Schertermin","Zwischen Augen und Pfoten regelmäßig kürzen"], + "schritte":["Täglich durchkämmen mit Metallkamm","Verfilzungen mit Finger lösen, dann Kamm","Alle 6–8 Wochen Schertermin","Zwischen Augen und Pfoten regelmäßig trimmen"], "materialien":"Metallkamm, Pin-Bürste, Schere","haeufigkeit":"Täglich + 6–8 Wochen Schertermin", - "fell_typ":"lockig","saison":None,"fell_pflege_art":"schneiden","tipp":"Lockiges Fell = hypoallergen, aber Pflegeaufwand unterschätzt!"}, - {"id":"fell_scheren_technik","titel":"Fell schneiden: Technik & Scheren-Tipps","kat":"Fell", - "beschreibung":"Für Rassen mit kontinuierlichem Fellwuchs (Pudel, Bichon, Spoodle) — Scheren statt Trimmen!", - "schritte":["Fell nach dem Bad komplett trocknen und bürsten","Schere oder Clipper parallel zur Haarwuchsrichtung führen","Empfindliche Stellen (Gesicht, Pfoten) mit Effilierschere","Alle 6–8 Wochen zum Groomer oder selbst lernen","Nach dem Scheren: Fell bürsten und kontrollieren"], - "materialien":"Effilierschere, Clipper, Metallkamm","haeufigkeit":"Alle 6–8 Wochen", - "fell_typ":"lockig","saison":None,"fell_pflege_art":"schneiden","tipp":"Nie nasses Fell scheren — immer erst trocknen, sonst ungleichmäßiges Ergebnis."}, - {"id":"fell_trimmen_technik","titel":"Fell trimmen: Stripping beim Rauhaar-Terrier","kat":"Fell", - "beschreibung":"Rauhaardrahtiges Fell (Westie, Schnauzer, Jack Russell) hat natürliche Wachstumsbegrenzung — Trimmen statt Scheren!", - "schritte":["Daumen und Zeigefinger: abgestorbene Haare zupfen (Stripping)","Immer in Haarwuchsrichtung arbeiten","Unterwolle mit feinem Rake ausbürsten","Niemals scheren — zerstört Textur dauerhaft","Alle 3–4 Monate professionelles Hand-Stripping"], - "materialien":"Stripper-Messer (Trimmmesser), Rake, Kreide für Grip","haeufigkeit":"Alle 3–4 Monate", - "fell_typ":"alle","saison":None,"fell_pflege_art":"trimmen","tipp":"Geschorenes Drahthaarfell verliert seine typische Textur dauerhaft — immer trimmen!"}, + "fell_typ":"lockig","saison":None,"tipp":"Lockiges Fell = hypoallergen, aber Pflegeaufwand unterschätzt!"}, {"id":"fell_unterwolle", "titel":"Unterwolle ausbürsten (Fellwechsel)","kat":"Fell", "beschreibung":"Zweimal jährlich toter Unterwolle-Berg — richtig ausgebürstet statt überall verteilt.", "schritte":["Undercoat-Rake gegen Haarwuchsrichtung","Abschnittweise: Rücken, Flanken, Bauch","Furminator maximal 2x/Woche","Nach dem Bürsten: Hund ausschütteln lassen"], @@ -959,28 +949,6 @@ async def _ki_complete(prompt: str) -> str: ) -async def _ki_complete_tracked(prompt: str, user_id: int) -> str: - """Wie _ki_complete, zählt die Anfrage in ki_daily_calls (source='luna').""" - import datetime as _dt - import ki as ki_module - text = await ki_module.complete( - prompt, - system=_SYSTEM, - max_tokens=1200, - requires_premium=False, - ) - today = str(_dt.date.today()) - try: - with db() as conn: - conn.execute(""" - INSERT INTO ki_daily_calls (user_id, date, count, source) VALUES (?, ?, 1, 'luna') - ON CONFLICT(user_id, date, source) DO UPDATE SET count = count + 1 - """, (user_id, today)) - except Exception: - pass - return text - - def _parse_json(raw: str) -> dict: import re try: @@ -1096,7 +1064,7 @@ async def generate_content(req: GenerateRequest, user=Depends(require_social_med ) try: - raw = await _ki_complete_tracked(prompt, user["id"]) + raw = await _ki_complete(prompt) data = _parse_json(raw) except Exception as e: logger.error("Social-Media-Generierung fehlgeschlagen: %s", e) @@ -1145,7 +1113,7 @@ async def evaluate_content(req: EvaluateRequest, user=Depends(require_social_med ) try: - raw = await _ki_complete_tracked(prompt, user["id"]) + raw = await _ki_complete(prompt) data = _parse_json(raw) except Exception as e: raise HTTPException(500, f"KI-Fehler: {e}") @@ -1229,21 +1197,13 @@ def _seed_pflege(): conn.execute( """INSERT OR IGNORE INTO pflege_tipps (tipp_id, titel, kategorie, beschreibung, schritte, - materialien, haeufigkeit, fell_typ, saison, rassengruppe, tipp, - fell_pflege_art) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", + materialien, haeufigkeit, fell_typ, saison, rassengruppe, tipp) + VALUES (?,?,?,?,?,?,?,?,?,?,?)""", (p["id"], p["titel"], p["kat"], p.get("beschreibung"), json.dumps(p.get("schritte", []), ensure_ascii=False), p.get("materialien"), p.get("haeufigkeit"), - p.get("fell_typ"), p.get("saison"), p.get("rassengruppe"), p.get("tipp"), - p.get("fell_pflege_art")), + p.get("fell_typ"), p.get("saison"), p.get("rassengruppe"), p.get("tipp")), ) - # UPDATE für bereits bestehende Tipps (idempotent) - if p.get("fell_pflege_art"): - conn.execute( - "UPDATE pflege_tipps SET fell_pflege_art=? WHERE tipp_id=? AND (fell_pflege_art IS NULL OR fell_pflege_art != ?)", - (p["fell_pflege_art"], p["id"], p["fell_pflege_art"]), - ) try: _seed_exercises() @@ -1291,7 +1251,7 @@ async def training_tip(user=Depends(require_social_media)): ) try: - raw = await _ki_complete_tracked(prompt, user["id"]) + raw = await _ki_complete(prompt) data = _parse_json(raw) except Exception as e: raise HTTPException(500, f"KI-Fehler: {e}") @@ -1447,7 +1407,7 @@ async def breed_of_day(user=Depends(require_social_media)): ) try: - raw = await _ki_complete_tracked(prompt, user["id"]) + raw = await _ki_complete(prompt) data = _parse_json(raw) except Exception as e: raise HTTPException(500, f"KI-Fehler: {e}") @@ -1586,7 +1546,7 @@ async def pflege_tipp(breed_id: Optional[int] = None, user=Depends(require_socia ) try: - raw = await _ki_complete_tracked(prompt, user["id"]) + raw = await _ki_complete(prompt) data = _parse_json(raw) except Exception as e: raise HTTPException(500, f"KI-Fehler: {e}") diff --git a/backend/routes/training.py b/backend/routes/training.py index b802a1b..c1f8dbc 100644 --- a/backend/routes/training.py +++ b/backend/routes/training.py @@ -777,10 +777,11 @@ async def ki_feedback(body: KiFeedbackRequest, user=Depends(get_current_user)): ).fetchone()[0] if age_hours < 6 and new_since == 0: - used = conn.execute( - "SELECT COALESCE(SUM(count),0) FROM ki_daily_calls WHERE user_id=? AND date=?", + daily_used = conn.execute( + "SELECT COALESCE(count,0) FROM ki_daily_calls WHERE user_id=? AND date=?", (uid, today) - ).fetchone()[0] + ).fetchone() + used = daily_used[0] if daily_used else 0 return {"feedback": cache_row["feedback"], "cached": True, "daily_used": used, "daily_limit": KI_DAILY_LIMIT} @@ -790,14 +791,14 @@ async def ki_feedback(body: KiFeedbackRequest, user=Depends(get_current_user)): user_id INTEGER NOT NULL, date TEXT NOT NULL, count INTEGER NOT NULL DEFAULT 0, - source TEXT NOT NULL DEFAULT 'cloud', - PRIMARY KEY (user_id, date, source) + PRIMARY KEY (user_id, date) ) """) - daily_used = conn.execute( - "SELECT COALESCE(SUM(count),0) FROM ki_daily_calls WHERE user_id=? AND date=?", + row = conn.execute( + "SELECT count FROM ki_daily_calls WHERE user_id=? AND date=?", (uid, today) - ).fetchone()[0] + ).fetchone() + daily_used = row[0] if row else 0 if daily_used >= KI_DAILY_LIMIT: raise HTTPException(429, f"Tages-Limit erreicht ({KI_DAILY_LIMIT} Anfragen/Tag). Morgen wieder verfügbar.") @@ -866,13 +867,12 @@ async def ki_feedback(body: KiFeedbackRequest, user=Depends(get_current_user)): ) try: - feedback_text, ki_source = await ki.complete( + feedback_text = await ki.complete( prompt, system=system, max_tokens=400, requires_premium=False, user_is_premium=user.get("is_premium", False), - return_source=True, ) except (ki.KIUnavailableError, ki.KIPremiumRequired) as e: raise HTTPException(503, str(e)) @@ -889,11 +889,11 @@ async def ki_feedback(body: KiFeedbackRequest, user=Depends(get_current_user)): (body.dog_id, feedback_text) ) conn.execute(""" - INSERT INTO ki_daily_calls (user_id, date, count, source) VALUES (?, ?, 1, ?) - ON CONFLICT(user_id, date, source) DO UPDATE SET count = count + 1 - """, (uid, today, ki_source)) + INSERT INTO ki_daily_calls (user_id, date, count) VALUES (?, ?, 1) + ON CONFLICT(user_id, date) DO UPDATE SET count = count + 1 + """, (uid, today)) new_count = conn.execute( - "SELECT COALESCE(SUM(count),0) FROM ki_daily_calls WHERE user_id=? AND date=?", + "SELECT count FROM ki_daily_calls WHERE user_id=? AND date=?", (uid, today) ).fetchone()[0] diff --git a/backend/scraper/fetch_wiki_images.py b/backend/scraper/fetch_wiki_images.py deleted file mode 100644 index 04b39eb..0000000 --- a/backend/scraper/fetch_wiki_images.py +++ /dev/null @@ -1,254 +0,0 @@ -""" -BAN YARO — Fehlende Rassen-Fotos von Wikipedia/Wikimedia holen - -Strategie: - 1. Alle Rassen ohne foto_url aus wiki_rassen holen - 2. Pro Rasse: Wikipedia pageimages API (de → en Fallback) - 3. Letzter Fallback: Wikimedia Commons pageimages API - 4. Sinnlose Bilder filtern (SVG, Flaggen-Icons, Karten, Logos) - 5. URL direkt in wiki_rassen.foto_url speichern - -CLI-Optionen: - --limit N Nur N Rassen bearbeiten (Default: 100) - --dry-run Nur anzeigen, nicht speichern - --model NAME Claude-Modell für ggf. zukünftige Text-Tasks - (Default: claude-sonnet-4-6) -""" - -import argparse -import asyncio -import logging -import os -import sys - -import httpx - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from database import db - -logger = logging.getLogger(__name__) - -_WP_HEADERS = { - "User-Agent": "Banyaro/1.0 (https://banyaro.de; mail@banyaro.de) httpx/Python" -} -_THUMB_SIZE = 600 - -# Dateinamen-Fragmente, die auf unbrauchbare Bilder hindeuten -_SKIP_PATTERNS = ( - ".svg", - "flag_of_", - "coat_of_arms", - "emblem_of_", - "location_map", - "orthographic_projection", - "locator_map", - "blank_map", - "wikimedia-logo", - "commons-logo", - "question_mark", - "noimage", -) - - -def _is_usable(url: str) -> bool: - """Gibt True zurück wenn die Bild-URL brauchbar erscheint.""" - low = url.lower() - if low.endswith(".svg"): - return False - for pattern in _SKIP_PATTERNS: - if pattern in low: - return False - return True - - -async def _fetch_wp_image(name: str, lang: str, client: httpx.AsyncClient) -> str | None: - """ - Fragt Wikipedia pageimages API für `name` in `lang` ab. - Gibt Thumbnail-URL zurück oder None. - """ - try: - resp = await client.get( - f"https://{lang}.wikipedia.org/w/api.php", - params={ - "action": "query", - "titles": name, - "prop": "pageimages", - "format": "json", - "pithumbsize": _THUMB_SIZE, - "redirects": 1, - }, - ) - resp.raise_for_status() - pages = resp.json().get("query", {}).get("pages", {}) - for page in pages.values(): - if page.get("pageid", -1) == -1: - continue - thumb = page.get("thumbnail", {}).get("source", "") - if thumb and _is_usable(thumb): - return thumb - except Exception as exc: - logger.debug("WP pageimages (%s/%s) Fehler: %s", lang, name, exc) - return None - - -async def _fetch_commons_image(name: str, client: httpx.AsyncClient) -> str | None: - """ - Fragt Wikimedia Commons pageimages API für `name` ab. - Wird als letzter Fallback genutzt. - """ - try: - resp = await client.get( - "https://commons.wikimedia.org/w/api.php", - params={ - "action": "query", - "titles": name, - "prop": "pageimages", - "format": "json", - "pithumbsize": _THUMB_SIZE, - }, - ) - resp.raise_for_status() - pages = resp.json().get("query", {}).get("pages", {}) - for page in pages.values(): - if page.get("pageid", -1) == -1: - continue - thumb = page.get("thumbnail", {}).get("source", "") - if thumb and _is_usable(thumb): - return thumb - except Exception as exc: - logger.debug("Commons pageimages (%s) Fehler: %s", name, exc) - return None - - -async def fetch_wiki_images(limit: int = 100, dry_run: bool = False) -> dict: - """ - Holt Wikipedia-Fotos für alle Rassen ohne foto_url. - - Returns: {'found': int, 'saved': int, 'missing': int} - """ - with db() as conn: - rows = conn.execute( - """SELECT id, name, name_de, slug - FROM wiki_rassen - WHERE (foto_url IS NULL OR foto_url = '') - ORDER BY name ASC - LIMIT ?""", - (limit,), - ).fetchall() - - total = len(rows) - if total == 0: - logger.info("Alle Rassen haben bereits ein Foto — nichts zu tun.") - return {"found": 0, "saved": 0, "missing": 0} - - logger.info("%d Rassen ohne Foto werden verarbeitet (limit=%d).", total, limit) - - found = 0 - saved = 0 - - async with httpx.AsyncClient( - timeout=12, - follow_redirects=True, - headers=_WP_HEADERS, - ) as client: - for idx, row in enumerate(rows, start=1): - name = row["name"] - name_de = row["name_de"] or "" - slug = row["slug"] or name - - # Suchreihenfolge: DE-Name → EN-Name → Commons mit EN-Name - candidates: list[tuple[str, str]] = [] - - if name_de: - candidates.append((name_de, "de")) - candidates.append((name, "en")) - if name_de: - candidates.append((name_de, "en")) - - foto_url: str | None = None - - for search_name, lang in candidates: - foto_url = await _fetch_wp_image(search_name, lang, client) - if foto_url: - logger.info( - "[%d/%d] ✓ %s → WP %s (%s)", - idx, total, name, lang.upper(), search_name, - ) - break - - # Letzter Fallback: Wikimedia Commons - if not foto_url: - foto_url = await _fetch_commons_image(name, client) - if foto_url: - logger.info( - "[%d/%d] ✓ %s → Commons", idx, total, name - ) - - if foto_url: - found += 1 - if dry_run: - logger.info(" [dry-run] würde setzen: %s", foto_url) - else: - try: - with db() as conn: - conn.execute( - "UPDATE wiki_rassen SET foto_url=? WHERE id=?", - (foto_url, row["id"]), - ) - saved += 1 - except Exception as exc: - logger.error("DB-Update fehlgeschlagen für %s: %s", name, exc) - else: - logger.info("[%d/%d] ✗ %s — kein Foto gefunden", idx, total, name) - - # Rate-Limit: 1 Sekunde zwischen Anfragen - await asyncio.sleep(1.0) - - missing = total - found - logger.info( - "Fertig: %d/%d Fotos gefunden, %d gespeichert, %d ohne Treffer.", - found, total, saved, missing, - ) - return {"found": found, "saved": saved, "missing": missing} - - -if __name__ == "__main__": - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(levelname)s %(message)s", - datefmt="%H:%M:%S", - ) - - parser = argparse.ArgumentParser( - description="Fehlende Rassen-Fotos von Wikipedia/Wikimedia holen" - ) - parser.add_argument( - "--limit", - type=int, - default=100, - metavar="N", - help="Maximale Anzahl Rassen (Default: 100)", - ) - parser.add_argument( - "--dry-run", - action="store_true", - help="Nur anzeigen, nicht in DB speichern", - ) - parser.add_argument( - "--model", - default="claude-sonnet-4-6", - metavar="MODEL", - help="Claude-Modell für Text-Tasks (Default: claude-sonnet-4-6)", - ) - args = parser.parse_args() - - if args.dry_run: - logger.info("DRY-RUN Modus — keine DB-Änderungen.") - - result = asyncio.run(fetch_wiki_images(limit=args.limit, dry_run=args.dry_run)) - print( - f"\nErgebnis: {result['found']} gefunden, " - f"{result['saved']} gespeichert, " - f"{result['missing']} ohne Treffer." - ) diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 74d9cc2..37733fd 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '345'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '344'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index b269f9e..4f1245d 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -207,18 +207,12 @@ window.Page_admin = (() => {
-

KI-Nutzung

+

KI-Trainer Nutzung (Claude API)

${[ - ['☁️ Claude heute', s.ki_cloud_today, 'var(--c-primary)'], - ['🖥️ LM Studio heute', s.ki_local_today, 'var(--c-success)'], - ['🌙 Luna heute', s.ki_luna_today, 'var(--c-warning)'], - ['Gesamt heute', s.ki_today, 'var(--c-text-secondary)'], - ['☁️ Claude Monat', s.ki_cloud_month, 'var(--c-primary)'], - ['🖥️ LM Studio Monat', s.ki_local_month, 'var(--c-success)'], - ['🌙 Luna Monat', s.ki_luna_month, 'var(--c-warning)'], - ['Gesamt Monat', s.ki_month, 'var(--c-text-secondary)'], - ['Aktive User heute', s.ki_users_today, 'var(--c-text-secondary)'], + ['Anfragen heute', s.ki_today, 'var(--c-primary)'], + ['Anfragen diesen Monat', s.ki_month, 'var(--c-text-secondary)'], + ['Aktive User heute', s.ki_users_today, 'var(--c-text-secondary)'], ].map(([label, val, color]) => `
${label} diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index 706d561..d21aeed 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -700,22 +700,6 @@ window.Page_diary = (() => {
-
- -
- - - - - - - - - -
@@ -760,6 +744,24 @@ window.Page_diary = (() => { ${entry?.is_milestone ? 'Meilenstein ✓' : 'Als Meilenstein markieren'}
+
+ + + +
+ + + + + + + + + +
`; diff --git a/backend/static/js/pages/dog-profile.js b/backend/static/js/pages/dog-profile.js index e9ed5b4..9a9df54 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -349,16 +349,6 @@ window.Page_dog_profile = (() => { 'Saisonal':'🌸','Gesundheitsvorsorge':'❤️','Welpen-Pflege':'🐶', }; - const pflegeArtBadge = data.fell_pflege_art === 'schneiden' - ? `✂️ Schneiden` - : data.fell_pflege_art === 'trimmen' - ? `✋ Trimmen` - : ''; - el.innerHTML = `
@@ -406,12 +396,11 @@ window.Page_dog_profile = (() => {