Compare commits
No commits in common. "5aba366b218a06584bd0da0dd7779fa9cc90ad46" and "74b6c03bb32b0bf9b0db4df0472a075ba8d11a68" have entirely different histories.
5aba366b21
...
74b6c03bb3
12 changed files with 62 additions and 441 deletions
|
|
@ -530,8 +530,6 @@ def _migrate(conn_factory):
|
||||||
("social_content", "exercise_id", "TEXT"),
|
("social_content", "exercise_id", "TEXT"),
|
||||||
("social_content", "post_url", "TEXT"),
|
("social_content", "post_url", "TEXT"),
|
||||||
("dogs", "rasse_id", "INTEGER"),
|
("dogs", "rasse_id", "INTEGER"),
|
||||||
# Pflege: Schere vs. Trimmen unterscheiden
|
|
||||||
("pflege_tipps", "fell_pflege_art", "TEXT"),
|
|
||||||
]
|
]
|
||||||
with conn_factory() as conn:
|
with conn_factory() as conn:
|
||||||
for table, column, col_type in migrations:
|
for table, column, col_type in migrations:
|
||||||
|
|
@ -1110,22 +1108,3 @@ def _migrate(conn_factory):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
logger.info("Migration: routes.is_valid bereit.")
|
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.")
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
|
||||||
KI_MODE = os.getenv("KI_MODE", "local") # off | local | cloud
|
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_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")
|
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", "")
|
ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||||
|
|
||||||
# Lazy Imports — nur laden wenn wirklich benötigt
|
# Lazy Imports — nur laden wenn wirklich benötigt
|
||||||
|
|
@ -70,7 +70,6 @@ async def complete(
|
||||||
user_is_premium: bool = False,
|
user_is_premium: bool = False,
|
||||||
json_mode: bool = False,
|
json_mode: bool = False,
|
||||||
return_model: bool = False,
|
return_model: bool = False,
|
||||||
return_source: bool = False,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
KI-Completion. Wählt automatisch den richtigen Backend.
|
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)
|
requires_premium: True = nur für Premium-User (nutzt Cloud)
|
||||||
user_is_premium: Ob der anfragende User Premium hat
|
user_is_premium: Ob der anfragende User Premium hat
|
||||||
json_mode: Antwort als JSON anfordern
|
json_mode: Antwort als JSON anfordern
|
||||||
return_source: Falls True: gibt (text, source) zurück, source = 'cloud'|'local'
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
KI-Antwort als String, oder (str, str) wenn return_source=True
|
KI-Antwort als String
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
KIPremiumRequired: Cloud-Feature ohne Premium
|
KIPremiumRequired: Cloud-Feature ohne Premium
|
||||||
|
|
@ -103,26 +101,20 @@ async def complete(
|
||||||
# Cloud-Aufruf: nur wenn Premium UND cloud-Modus
|
# Cloud-Aufruf: nur wenn Premium UND cloud-Modus
|
||||||
if requires_premium and user_is_premium and KI_MODE == "cloud":
|
if requires_premium and user_is_premium and KI_MODE == "cloud":
|
||||||
text = await _cloud_complete(prompt, system, max_tokens, json_mode)
|
text = await _cloud_complete(prompt, system, max_tokens, json_mode)
|
||||||
if return_model:
|
return (text, CLOUD_MODEL) if return_model else text
|
||||||
return (text, CLOUD_MODEL)
|
|
||||||
return (text, "cloud") if return_source else text
|
|
||||||
|
|
||||||
# Lokaler Aufruf: Entwicklung + Free-User
|
# Lokaler Aufruf: Entwicklung + Free-User
|
||||||
if KI_MODE in ("local", "cloud"):
|
if KI_MODE in ("local", "cloud"):
|
||||||
try:
|
try:
|
||||||
text = await _local_complete(prompt, system, max_tokens, json_mode)
|
text = await _local_complete(prompt, system, max_tokens, json_mode)
|
||||||
if return_model:
|
return (text, LOCAL_MODEL) if return_model else text
|
||||||
return (text, LOCAL_MODEL)
|
|
||||||
return (text, "local") if return_source else text
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Lokales KI-Modell nicht erreichbar: {e}")
|
logger.warning(f"Lokales KI-Modell nicht erreichbar: {e}")
|
||||||
# Cloud-Fallback: im cloud-Modus immer, sonst nur für Premium-User
|
# 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)):
|
if ANTHROPIC_KEY and (KI_MODE == "cloud" or (requires_premium and user_is_premium)):
|
||||||
logger.info("Fallback auf Cloud-KI.")
|
logger.info("Fallback auf Cloud-KI.")
|
||||||
text = await _cloud_complete(prompt, system, max_tokens, json_mode)
|
text = await _cloud_complete(prompt, system, max_tokens, json_mode)
|
||||||
if return_model:
|
return (text, CLOUD_MODEL) if return_model else text
|
||||||
return (text, CLOUD_MODEL)
|
|
||||||
return (text, "cloud") if return_source else text
|
|
||||||
raise KIUnavailableError(
|
raise KIUnavailableError(
|
||||||
"KI-Modell momentan nicht erreichbar. Bitte später erneut versuchen."
|
"KI-Modell momentan nicht erreichbar. Bitte später erneut versuchen."
|
||||||
) from e
|
) from e
|
||||||
|
|
|
||||||
|
|
@ -160,30 +160,8 @@ async def stats(user=Depends(require_mod)):
|
||||||
ki_users_today = conn.execute(
|
ki_users_today = conn.execute(
|
||||||
"SELECT COUNT(DISTINCT user_id) FROM ki_daily_calls WHERE date=DATE('now')"
|
"SELECT COUNT(DISTINCT user_id) FROM ki_daily_calls WHERE date=DATE('now')"
|
||||||
).fetchone()[0]
|
).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:
|
except Exception:
|
||||||
ki_today = ki_month = ki_users_today = 0
|
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
|
# Social Media Tracking
|
||||||
try:
|
try:
|
||||||
|
|
@ -239,12 +217,6 @@ async def stats(user=Depends(require_mod)):
|
||||||
"ki_today": ki_today,
|
"ki_today": ki_today,
|
||||||
"ki_month": ki_month,
|
"ki_month": ki_month,
|
||||||
"ki_users_today": ki_users_today,
|
"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_total": social_total,
|
||||||
"social_published": social_published,
|
"social_published": social_published,
|
||||||
"social_scheduled": social_scheduled,
|
"social_scheduled": social_scheduled,
|
||||||
|
|
|
||||||
|
|
@ -361,9 +361,8 @@ async def get_pflege_tipps(dog_id: int, user=Depends(get_current_user)):
|
||||||
(f"%{dog['rasse']}%",)
|
(f"%{dog['rasse']}%",)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
# Fell-Typ und Pflegeart ableiten
|
# Fell-Typ ableiten
|
||||||
fell_filter = None
|
fell_filter = None
|
||||||
fell_pflege_art_filter = None
|
|
||||||
if rasse_info:
|
if rasse_info:
|
||||||
beschr = (rasse_info["beschreibung"] or "").lower()
|
beschr = (rasse_info["beschreibung"] or "").lower()
|
||||||
if any(w in beschr for w in ["lockig", "wellig", "kraus", "pudel", "doodle"]):
|
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"):
|
elif rasse_info["groesse"] in ("gross", "sehr_gross"):
|
||||||
fell_filter = "doppel"
|
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:
|
with db() as conn:
|
||||||
alle_tipps = conn.execute(
|
alle_tipps = conn.execute(
|
||||||
"SELECT * FROM pflege_tipps ORDER BY kategorie, titel"
|
"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 = []
|
result = []
|
||||||
for t in alle_tipps:
|
for t in alle_tipps:
|
||||||
t = dict(t)
|
t = dict(t)
|
||||||
# Fell-Typ-Filter
|
# Fell-Filter
|
||||||
if fell_filter and t["fell_typ"] and t["fell_typ"] != "alle":
|
if fell_filter and t["fell_typ"] and t["fell_typ"] != "alle":
|
||||||
if fell_filter not in t["fell_typ"].split(","):
|
if fell_filter not in t["fell_typ"].split(","):
|
||||||
continue
|
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["schritte"] = _json.loads(t["schritte"] or "[]")
|
||||||
t["saisonal_aktuell"] = bool(t["saison"] and heute_saison in t["saison"])
|
t["saisonal_aktuell"] = bool(t["saison"] and heute_saison in t["saison"])
|
||||||
result.append(t)
|
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
|
tipp_des_tages = (saisonal or result)[day_hash % len(saisonal or result)] if result else None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"dog_name": dog["name"],
|
"dog_name": dog["name"],
|
||||||
"rasse_name": rasse_info["name"] if rasse_info else dog["rasse"],
|
"rasse_name": rasse_info["name"] if rasse_info else dog["rasse"],
|
||||||
"tipp_des_tages": tipp_des_tages,
|
"tipp_des_tages": tipp_des_tages,
|
||||||
"tipps": result,
|
"tipps": result,
|
||||||
"kategorien": list(dict.fromkeys(t["kategorie"] for t in result)),
|
"kategorien": list(dict.fromkeys(t["kategorie"] for t in result)),
|
||||||
"fell_pflege_art": fell_pflege_art_filter, # 'schneiden' | 'trimmen' | None
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -466,19 +466,9 @@ _PFLEGE_TIPPS = [
|
||||||
"fell_typ":"lang","saison":None,"tipp":"Von Beinen und Schwanz beginnen — dort filzt es zuerst."},
|
"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",
|
{"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.",
|
"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",
|
"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!"},
|
"fell_typ":"lockig","saison":None,"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!"},
|
|
||||||
{"id":"fell_unterwolle", "titel":"Unterwolle ausbürsten (Fellwechsel)","kat":"Fell",
|
{"id":"fell_unterwolle", "titel":"Unterwolle ausbürsten (Fellwechsel)","kat":"Fell",
|
||||||
"beschreibung":"Zweimal jährlich toter Unterwolle-Berg — richtig ausgebürstet statt überall verteilt.",
|
"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"],
|
"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:
|
def _parse_json(raw: str) -> dict:
|
||||||
import re
|
import re
|
||||||
try:
|
try:
|
||||||
|
|
@ -1096,7 +1064,7 @@ async def generate_content(req: GenerateRequest, user=Depends(require_social_med
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw = await _ki_complete_tracked(prompt, user["id"])
|
raw = await _ki_complete(prompt)
|
||||||
data = _parse_json(raw)
|
data = _parse_json(raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Social-Media-Generierung fehlgeschlagen: %s", 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:
|
try:
|
||||||
raw = await _ki_complete_tracked(prompt, user["id"])
|
raw = await _ki_complete(prompt)
|
||||||
data = _parse_json(raw)
|
data = _parse_json(raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(500, f"KI-Fehler: {e}")
|
raise HTTPException(500, f"KI-Fehler: {e}")
|
||||||
|
|
@ -1229,21 +1197,13 @@ def _seed_pflege():
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT OR IGNORE INTO pflege_tipps
|
"""INSERT OR IGNORE INTO pflege_tipps
|
||||||
(tipp_id, titel, kategorie, beschreibung, schritte,
|
(tipp_id, titel, kategorie, beschreibung, schritte,
|
||||||
materialien, haeufigkeit, fell_typ, saison, rassengruppe, tipp,
|
materialien, haeufigkeit, fell_typ, saison, rassengruppe, tipp)
|
||||||
fell_pflege_art)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
||||||
(p["id"], p["titel"], p["kat"],
|
(p["id"], p["titel"], p["kat"],
|
||||||
p.get("beschreibung"), json.dumps(p.get("schritte", []), ensure_ascii=False),
|
p.get("beschreibung"), json.dumps(p.get("schritte", []), ensure_ascii=False),
|
||||||
p.get("materialien"), p.get("haeufigkeit"),
|
p.get("materialien"), p.get("haeufigkeit"),
|
||||||
p.get("fell_typ"), p.get("saison"), p.get("rassengruppe"), p.get("tipp"),
|
p.get("fell_typ"), p.get("saison"), p.get("rassengruppe"), p.get("tipp")),
|
||||||
p.get("fell_pflege_art")),
|
|
||||||
)
|
)
|
||||||
# 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:
|
try:
|
||||||
_seed_exercises()
|
_seed_exercises()
|
||||||
|
|
@ -1291,7 +1251,7 @@ async def training_tip(user=Depends(require_social_media)):
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw = await _ki_complete_tracked(prompt, user["id"])
|
raw = await _ki_complete(prompt)
|
||||||
data = _parse_json(raw)
|
data = _parse_json(raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(500, f"KI-Fehler: {e}")
|
raise HTTPException(500, f"KI-Fehler: {e}")
|
||||||
|
|
@ -1447,7 +1407,7 @@ async def breed_of_day(user=Depends(require_social_media)):
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw = await _ki_complete_tracked(prompt, user["id"])
|
raw = await _ki_complete(prompt)
|
||||||
data = _parse_json(raw)
|
data = _parse_json(raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(500, f"KI-Fehler: {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:
|
try:
|
||||||
raw = await _ki_complete_tracked(prompt, user["id"])
|
raw = await _ki_complete(prompt)
|
||||||
data = _parse_json(raw)
|
data = _parse_json(raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(500, f"KI-Fehler: {e}")
|
raise HTTPException(500, f"KI-Fehler: {e}")
|
||||||
|
|
|
||||||
|
|
@ -777,10 +777,11 @@ async def ki_feedback(body: KiFeedbackRequest, user=Depends(get_current_user)):
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
|
|
||||||
if age_hours < 6 and new_since == 0:
|
if age_hours < 6 and new_since == 0:
|
||||||
used = conn.execute(
|
daily_used = conn.execute(
|
||||||
"SELECT COALESCE(SUM(count),0) FROM ki_daily_calls WHERE user_id=? AND date=?",
|
"SELECT COALESCE(count,0) FROM ki_daily_calls WHERE user_id=? AND date=?",
|
||||||
(uid, today)
|
(uid, today)
|
||||||
).fetchone()[0]
|
).fetchone()
|
||||||
|
used = daily_used[0] if daily_used else 0
|
||||||
return {"feedback": cache_row["feedback"], "cached": True,
|
return {"feedback": cache_row["feedback"], "cached": True,
|
||||||
"daily_used": used, "daily_limit": KI_DAILY_LIMIT}
|
"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,
|
user_id INTEGER NOT NULL,
|
||||||
date TEXT NOT NULL,
|
date TEXT NOT NULL,
|
||||||
count INTEGER NOT NULL DEFAULT 0,
|
count INTEGER NOT NULL DEFAULT 0,
|
||||||
source TEXT NOT NULL DEFAULT 'cloud',
|
PRIMARY KEY (user_id, date)
|
||||||
PRIMARY KEY (user_id, date, source)
|
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
daily_used = conn.execute(
|
row = 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)
|
(uid, today)
|
||||||
).fetchone()[0]
|
).fetchone()
|
||||||
|
daily_used = row[0] if row else 0
|
||||||
|
|
||||||
if daily_used >= KI_DAILY_LIMIT:
|
if daily_used >= KI_DAILY_LIMIT:
|
||||||
raise HTTPException(429, f"Tages-Limit erreicht ({KI_DAILY_LIMIT} Anfragen/Tag). Morgen wieder verfügbar.")
|
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:
|
try:
|
||||||
feedback_text, ki_source = await ki.complete(
|
feedback_text = await ki.complete(
|
||||||
prompt,
|
prompt,
|
||||||
system=system,
|
system=system,
|
||||||
max_tokens=400,
|
max_tokens=400,
|
||||||
requires_premium=False,
|
requires_premium=False,
|
||||||
user_is_premium=user.get("is_premium", False),
|
user_is_premium=user.get("is_premium", False),
|
||||||
return_source=True,
|
|
||||||
)
|
)
|
||||||
except (ki.KIUnavailableError, ki.KIPremiumRequired) as e:
|
except (ki.KIUnavailableError, ki.KIPremiumRequired) as e:
|
||||||
raise HTTPException(503, str(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)
|
(body.dog_id, feedback_text)
|
||||||
)
|
)
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
INSERT INTO ki_daily_calls (user_id, date, count, source) VALUES (?, ?, 1, ?)
|
INSERT INTO ki_daily_calls (user_id, date, count) VALUES (?, ?, 1)
|
||||||
ON CONFLICT(user_id, date, source) DO UPDATE SET count = count + 1
|
ON CONFLICT(user_id, date) DO UPDATE SET count = count + 1
|
||||||
""", (uid, today, ki_source))
|
""", (uid, today))
|
||||||
new_count = conn.execute(
|
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)
|
(uid, today)
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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."
|
|
||||||
)
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
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 = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,18 +207,12 @@ window.Page_admin = (() => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="padding:var(--space-4)">
|
<div class="card" style="padding:var(--space-4)">
|
||||||
<p style="font-size:var(--text-sm);font-weight:600;margin:0 0 var(--space-3)">KI-Nutzung</p>
|
<p style="font-size:var(--text-sm);font-weight:600;margin:0 0 var(--space-3)">KI-Trainer Nutzung (Claude API)</p>
|
||||||
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||||||
${[
|
${[
|
||||||
['☁️ Claude heute', s.ki_cloud_today, 'var(--c-primary)'],
|
['Anfragen heute', s.ki_today, 'var(--c-primary)'],
|
||||||
['🖥️ LM Studio heute', s.ki_local_today, 'var(--c-success)'],
|
['Anfragen diesen Monat', s.ki_month, 'var(--c-text-secondary)'],
|
||||||
['🌙 Luna heute', s.ki_luna_today, 'var(--c-warning)'],
|
['Aktive User heute', s.ki_users_today, 'var(--c-text-secondary)'],
|
||||||
['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)'],
|
|
||||||
].map(([label, val, color]) => `
|
].map(([label, val, color]) => `
|
||||||
<div style="display:flex;justify-content:space-between;font-size:var(--text-sm)">
|
<div style="display:flex;justify-content:space-between;font-size:var(--text-sm)">
|
||||||
<span style="color:var(--c-text-secondary)">${label}</span>
|
<span style="color:var(--c-text-secondary)">${label}</span>
|
||||||
|
|
|
||||||
|
|
@ -700,22 +700,6 @@ window.Page_diary = (() => {
|
||||||
<textarea class="form-control" name="text" rows="5"
|
<textarea class="form-control" name="text" rows="5"
|
||||||
placeholder="Was ist passiert? Besonderheiten, Gedanken…">${UI.escape(entry?.text || '')}</textarea>
|
placeholder="Was ist passiert? Besonderheiten, Gedanken…">${UI.escape(entry?.text || '')}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<!-- Bestehende Medien (Edit-Modus) -->
|
|
||||||
<div id="diary-existing-media"></div>
|
|
||||||
|
|
||||||
<!-- Neue Medien: Vorschau-Grid -->
|
|
||||||
<div id="diary-new-media-grid" class="diary-media-grid" style="display:none"></div>
|
|
||||||
|
|
||||||
<!-- versteckter Input — multiple für Mehrfachauswahl -->
|
|
||||||
<input type="file" id="diary-media-input" accept="image/*,video/*,application/pdf" multiple style="display:none">
|
|
||||||
|
|
||||||
<!-- Einzelner Button — iOS zeigt nativen Picker (Mediathek / Kamera / Datei) -->
|
|
||||||
<label for="diary-media-input" class="btn btn-secondary" style="cursor:pointer;display:flex;align-items:center;gap:var(--space-2);justify-content:center">
|
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#image"></use></svg>
|
|
||||||
Fotos / Videos hinzufügen
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" id="diary-location-group">
|
<div class="form-group" id="diary-location-group">
|
||||||
<label class="form-label">Ort <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
<label class="form-label">Ort <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
||||||
|
|
||||||
|
|
@ -760,6 +744,24 @@ window.Page_diary = (() => {
|
||||||
<span>${entry?.is_milestone ? 'Meilenstein ✓' : 'Als Meilenstein markieren'}</span>
|
<span>${entry?.is_milestone ? 'Meilenstein ✓' : 'Als Meilenstein markieren'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Fotos / Videos <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
||||||
|
|
||||||
|
<!-- Bestehende Medien (Edit-Modus) -->
|
||||||
|
<div id="diary-existing-media"></div>
|
||||||
|
|
||||||
|
<!-- Neue Medien: Vorschau-Grid -->
|
||||||
|
<div id="diary-new-media-grid" class="diary-media-grid" style="display:none"></div>
|
||||||
|
|
||||||
|
<!-- versteckter Input — multiple für Mehrfachauswahl -->
|
||||||
|
<input type="file" id="diary-media-input" accept="image/*,video/*,application/pdf" multiple style="display:none">
|
||||||
|
|
||||||
|
<!-- Einzelner Button — iOS zeigt nativen Picker (Mediathek / Kamera / Datei) -->
|
||||||
|
<label for="diary-media-input" class="btn btn-secondary" style="margin-top:var(--space-2);cursor:pointer;display:flex;align-items:center;gap:var(--space-2);justify-content:center">
|
||||||
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#image"></use></svg>
|
||||||
|
Fotos / Videos hinzufügen
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -349,16 +349,6 @@ window.Page_dog_profile = (() => {
|
||||||
'Saisonal':'🌸','Gesundheitsvorsorge':'❤️','Welpen-Pflege':'🐶',
|
'Saisonal':'🌸','Gesundheitsvorsorge':'❤️','Welpen-Pflege':'🐶',
|
||||||
};
|
};
|
||||||
|
|
||||||
const pflegeArtBadge = data.fell_pflege_art === 'schneiden'
|
|
||||||
? `<span title="Dieses Fell wächst kontinuierlich und wird mit der Schere geschnitten"
|
|
||||||
style="font-size:10px;font-weight:700;padding:2px 7px;border-radius:20px;
|
|
||||||
background:#dbeafe;color:#1d4ed8;margin-left:6px">✂️ Schneiden</span>`
|
|
||||||
: data.fell_pflege_art === 'trimmen'
|
|
||||||
? `<span title="Dieses Fell hat natürliche Wachstumsbegrenzung und wird durch Hand-Stripping gepflegt"
|
|
||||||
style="font-size:10px;font-weight:700;padding:2px 7px;border-radius:20px;
|
|
||||||
background:#fef9c3;color:#92400e;margin-left:6px">✋ Trimmen</span>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="card" style="padding:var(--space-4)">
|
<div class="card" style="padding:var(--space-4)">
|
||||||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
|
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
|
||||||
|
|
@ -406,12 +396,11 @@ window.Page_dog_profile = (() => {
|
||||||
<div id="dp-pflege-liste" style="display:none;margin-top:var(--space-3)">
|
<div id="dp-pflege-liste" style="display:none;margin-top:var(--space-3)">
|
||||||
${data.kategorien.map(kat => {
|
${data.kategorien.map(kat => {
|
||||||
const katTipps = data.tipps.filter(t=>t.kategorie===kat);
|
const katTipps = data.tipps.filter(t=>t.kategorie===kat);
|
||||||
const katBadge = kat === 'Fell' ? pflegeArtBadge : '';
|
|
||||||
return `
|
return `
|
||||||
<div style="margin-bottom:var(--space-3)">
|
<div style="margin-bottom:var(--space-3)">
|
||||||
<div style="font-size:11px;font-weight:700;color:var(--c-text-muted);
|
<div style="font-size:11px;font-weight:700;color:var(--c-text-muted);
|
||||||
text-transform:uppercase;margin-bottom:8px;display:flex;align-items:center">
|
text-transform:uppercase;margin-bottom:8px">
|
||||||
${kat_icons[kat]||'🐾'} ${_esc(kat)}${katBadge}</div>
|
${kat_icons[kat]||'🐾'} ${_esc(kat)}</div>
|
||||||
${katTipps.map(tip => `
|
${katTipps.map(tip => `
|
||||||
<details style="background:var(--c-surface-2);border-radius:8px;
|
<details style="background:var(--c-surface-2);border-radius:8px;
|
||||||
padding:10px;margin-bottom:6px">
|
padding:10px;margin-bottom:6px">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v360';
|
const CACHE_VERSION = 'by-v359';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue