Pflege-System: Pflegetipps im Hundeprofil + Rassen-Autocomplete
- GET /api/dogs/{id}/pflege: rassenspezifische Pflegetipps
- pflege_tipps DB-Tabelle (43 Tipps, 10 Kategorien) geseedet
- dogs.rasse_id für Wiki-Verknüpfung (Migration)
- Hundeprofil: Tipp des Tages + alle Tipps aufklappbar
- Hundeprofil-Edit: Rassen-Autocomplete mit Wiki-Match-Badge
- Social: Post-Bestätigung (Gepostet!-Button, Quick-Mark, Pending-Banner)
- Social: Pflegetipp-Button (allg. + rassenspezifisch)
- Social: Diversitäts-Check, Kategorie-Tagging
- Social: 104 Übungen, Übungsübersicht-Modal
- Admin: Social-Media-Tracking-Sektion
- SW by-v356, APP_VER 343
This commit is contained in:
parent
75615140c4
commit
ba5547f993
7 changed files with 797 additions and 9 deletions
|
|
@ -528,6 +528,8 @@ def _migrate(conn_factory):
|
||||||
("social_content", "media_url", "TEXT"),
|
("social_content", "media_url", "TEXT"),
|
||||||
("social_content", "category", "TEXT"),
|
("social_content", "category", "TEXT"),
|
||||||
("social_content", "exercise_id", "TEXT"),
|
("social_content", "exercise_id", "TEXT"),
|
||||||
|
("social_content", "post_url", "TEXT"),
|
||||||
|
("dogs", "rasse_id", "INTEGER"),
|
||||||
]
|
]
|
||||||
with conn_factory() as conn:
|
with conn_factory() as conn:
|
||||||
for table, column, col_type in migrations:
|
for table, column, col_type in migrations:
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ class DogCreate(BaseModel):
|
||||||
class DogUpdate(BaseModel):
|
class DogUpdate(BaseModel):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
rasse: Optional[str] = None
|
rasse: Optional[str] = None
|
||||||
|
rasse_id: Optional[int] = None
|
||||||
geburtstag: Optional[str] = None
|
geburtstag: Optional[str] = None
|
||||||
geschlecht: Optional[str] = None
|
geschlecht: Optional[str] = None
|
||||||
gewicht_kg: Optional[float] = None
|
gewicht_kg: Optional[float] = None
|
||||||
|
|
@ -330,3 +331,81 @@ async def report_found(dog_id: int, data: FoundReport = FoundReport()):
|
||||||
})
|
})
|
||||||
|
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/dogs/{id}/pflege — Pflegetipps für diesen Hund
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/{dog_id}/pflege")
|
||||||
|
async def get_pflege_tipps(dog_id: int, user=Depends(get_current_user)):
|
||||||
|
import json as _json
|
||||||
|
with db() as conn:
|
||||||
|
dog = conn.execute(
|
||||||
|
"SELECT id, name, rasse, rasse_id FROM dogs WHERE id=? AND user_id=?",
|
||||||
|
(dog_id, user["id"])
|
||||||
|
).fetchone()
|
||||||
|
if not dog:
|
||||||
|
raise HTTPException(404, "Hund nicht gefunden.")
|
||||||
|
|
||||||
|
# Rassen-Infos für Fell-Typ
|
||||||
|
rasse_info = None
|
||||||
|
with db() as conn:
|
||||||
|
if dog["rasse_id"]:
|
||||||
|
rasse_info = conn.execute(
|
||||||
|
"SELECT name, groesse, beschreibung FROM wiki_rassen WHERE id=?",
|
||||||
|
(dog["rasse_id"],)
|
||||||
|
).fetchone()
|
||||||
|
elif dog["rasse"]:
|
||||||
|
rasse_info = conn.execute(
|
||||||
|
"SELECT name, groesse, beschreibung FROM wiki_rassen WHERE name LIKE ? LIMIT 1",
|
||||||
|
(f"%{dog['rasse']}%",)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
# Fell-Typ ableiten
|
||||||
|
fell_filter = None
|
||||||
|
if rasse_info:
|
||||||
|
beschr = (rasse_info["beschreibung"] or "").lower()
|
||||||
|
if any(w in beschr for w in ["lockig", "wellig", "kraus", "pudel", "doodle"]):
|
||||||
|
fell_filter = "lockig"
|
||||||
|
elif any(w in beschr for w in ["langhaar", "seidiges", "fließendes", "langes fell"]):
|
||||||
|
fell_filter = "lang"
|
||||||
|
elif any(w in beschr for w in ["kurzhaar", "kurzes fell", "glatthaarig"]):
|
||||||
|
fell_filter = "kurz"
|
||||||
|
elif rasse_info["groesse"] in ("gross", "sehr_gross"):
|
||||||
|
fell_filter = "doppel"
|
||||||
|
|
||||||
|
with db() as conn:
|
||||||
|
alle_tipps = conn.execute(
|
||||||
|
"SELECT * FROM pflege_tipps ORDER BY kategorie, titel"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
# Relevante Tipps: kein Fell-Filter oder passend
|
||||||
|
from datetime import date
|
||||||
|
heute_saison = {1:"winter",2:"winter",3:"fruehling",4:"fruehling",5:"fruehling",
|
||||||
|
6:"sommer",7:"sommer",8:"sommer",9:"herbst",10:"herbst",
|
||||||
|
11:"herbst",12:"winter"}[date.today().month]
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for t in alle_tipps:
|
||||||
|
t = dict(t)
|
||||||
|
# Fell-Filter
|
||||||
|
if fell_filter and t["fell_typ"] and t["fell_typ"] != "alle":
|
||||||
|
if fell_filter not in t["fell_typ"].split(","):
|
||||||
|
continue
|
||||||
|
t["schritte"] = _json.loads(t["schritte"] or "[]")
|
||||||
|
t["saisonal_aktuell"] = bool(t["saison"] and heute_saison in t["saison"])
|
||||||
|
result.append(t)
|
||||||
|
|
||||||
|
# Tipp des Tages: erster aktuell-saisonaler oder zufällig deterministisch
|
||||||
|
from hashlib import md5
|
||||||
|
day_hash = int(md5(str(date.today()).encode()).hexdigest(), 16)
|
||||||
|
saisonal = [t for t in result if t["saisonal_aktuell"]]
|
||||||
|
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)),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,264 @@ _UEBUNGEN = [
|
||||||
"tipp": "Jede neue Begegnung in dieser Phase prägt lebenslang."},
|
"tipp": "Jede neue Begegnung in dieser Phase prägt lebenslang."},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Pflege-Tipps-Bibliothek
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
_PFLEGE_TIPPS = [
|
||||||
|
# ── FELL ────────────────────────────────────────────────────────
|
||||||
|
{"id":"fell_buersten_kurz","titel":"Bürsten bei Kurzhaarfell","kat":"Fell",
|
||||||
|
"beschreibung":"Kurzhaar braucht weniger, aber regelmäßige Pflege — für Glanz und Gesundheit.",
|
||||||
|
"schritte":["Gummibürste oder Hundekamm","Gegen und mit dem Fell bürsten","Totreste entfernen","Glanzspray optional"],
|
||||||
|
"materialien":"Gummibürste, feiner Kamm, Mikrofasertuch","haeufigkeit":"1–2x pro Woche",
|
||||||
|
"fell_typ":"kurz","saison":None,"tipp":"Kurzhaar verliert trotzdem Haare — wöchentliches Bürsten hält Wohnung sauber."},
|
||||||
|
{"id":"fell_buersten_lang","titel":"Bürsten bei Langhaarfell","kat":"Fell",
|
||||||
|
"beschreibung":"Langhaar verfilzt schnell — tägliches Bürsten verhindert schmerzhafte Knoten.",
|
||||||
|
"schritte":["Abschnittweise arbeiten","Zuerst Unterwolle mit Undercoat-Rake","Dann Bürste über alles","Verfilzungen mit Entfilzer-Spray lösen, nie reißen"],
|
||||||
|
"materialien":"Undercoat-Rake, Slicker-Bürste, Entfilzer-Spray","haeufigkeit":"Täglich",
|
||||||
|
"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 trimmen"],
|
||||||
|
"materialien":"Metallkamm, Pin-Bürste, Schere","haeufigkeit":"Täglich + 6–8 Wochen Schertermin",
|
||||||
|
"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"],
|
||||||
|
"materialien":"Furminator oder Undercoat-Rake","haeufigkeit":"Täglich während Fellwechsel",
|
||||||
|
"fell_typ":"doppel","saison":"fruehling,herbst","tipp":"Einen Schuh voll Haare pro Session ist normal bei Huskies & Co."},
|
||||||
|
{"id":"fell_bad_timing", "titel":"Wann und wie oft Baden?","kat":"Fell",
|
||||||
|
"beschreibung":"Zu häufiges Baden zerstört den natürlichen Fellschutz — die richtige Balance.",
|
||||||
|
"schritte":["Kurzhaar: alle 6–8 Wochen","Langhaar/Lockig: alle 4–6 Wochen","Vor dem Bad: komplett ausbürsten","Nur Hundeshampoo verwenden — Menschen-Shampoo macht Fell spröde"],
|
||||||
|
"materialien":"Hundeshampoo, Handbrause, große Handtücher","haeufigkeit":"4–8 Wochen",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Fön auf niedrigster Stufe — hohe Hitze schadet dem Fell."},
|
||||||
|
{"id":"fell_trocknen", "titel":"Richtiges Trocknen nach dem Bad","kat":"Fell",
|
||||||
|
"beschreibung":"Nasses Fell eingerollt schläft = Hautprobleme — so trocknet man richtig.",
|
||||||
|
"schritte":["Handtuch: klopfen, nicht reiben", "Hundeföhn oder normaler Fön auf Kalt/Niedrig","Fell gleichzeitig bürsten","Niemals nass in kalte Luft"],
|
||||||
|
"materialien":"Absorbier-Handtuch, Hundeföhn","haeufigkeit":"Nach jedem Bad",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Mikrofaser-Handtücher nehmen 5x mehr Wasser auf als normale."},
|
||||||
|
{"id":"fell_geruch", "titel":"Fell-Geruch zwischen Bädern","kat":"Fell",
|
||||||
|
"beschreibung":"Hund riecht ohne Baden — Trockenshampoo und Hausmittel helfen.",
|
||||||
|
"schritte":["Trockenshampoo einsprühen","3 Min einwirken lassen","Ausbürsten","Alternative: Backpulver einmassieren, ausklopfen"],
|
||||||
|
"materialien":"Trockenshampoo für Hunde, Bürste","haeufigkeit":"Nach Bedarf",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Geruch oft durch Unterkieferdrüsen — dort bürsten!"},
|
||||||
|
{"id":"fell_ernaehrung", "titel":"Ernährung für gesundes, glänzendes Fell","kat":"Fell",
|
||||||
|
"beschreibung":"Schönes Fell kommt von innen — die richtigen Nährstoffe machen den Unterschied.",
|
||||||
|
"schritte":["Lachsöl täglich: 1 TL pro 10 kg","Omega-3-Fettsäuren im Futter","Hochwertige Proteine","Biotin-Supplement bei stumpfem Fell"],
|
||||||
|
"materialien":"Lachsöl, hochwertiges Futter","haeufigkeit":"Täglich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Verbesserung sichtbar nach 4–8 Wochen."},
|
||||||
|
# ── KRALLEN ─────────────────────────────────────────────────────
|
||||||
|
{"id":"krallen_schneiden", "titel":"Krallen richtig schneiden","kat":"Krallen",
|
||||||
|
"beschreibung":"Zu lange Krallen verändern die Körperhaltung und können schmerzhaft sein.",
|
||||||
|
"schritte":["Ruhige Umgebung, Hund entspannt","Krallenschneider scharf und sauber","Weißes Fell: rosa Bereich (Quick) sehen → 2mm davor schneiden","Schwarze Krallen: kleine Scheiben bis weiß/grau sichtbar"],
|
||||||
|
"materialien":"Krallenschneider oder -feile, Blutungspulver für Notfall","haeufigkeit":"Alle 3–4 Wochen",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Schneidet man zu kurz: Blutung sofort mit Stärke oder Kaffeepulver stoppen."},
|
||||||
|
{"id":"krallen_schleifen", "titel":"Krallen-Schleifbrett trainieren","kat":"Krallen",
|
||||||
|
"beschreibung":"Hund schleift Krallen selbst ab — nie wieder Krallen-Kampf!",
|
||||||
|
"schritte":["Schleifbrett (Sandpapier auf Brett) vorlegen","Pfote draufstellen belohnen","Kratzbewegung formen","Hund macht es selbst auf Kommando"],
|
||||||
|
"materialien":"Kratzbrett selbstgebaut: Holz + grobes Sandpapier","haeufigkeit":"Wöchentlich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Hinterkrallen schleifen sich beim Laufen oft selbst ab."},
|
||||||
|
{"id":"krallen_dew", "titel":"Daumenkralle / Wolfskralle","kat":"Krallen",
|
||||||
|
"beschreibung":"Die Wolfskralle berührt keinen Boden — wächst ins Fleisch wenn vergessen!",
|
||||||
|
"schritte":["Wolfskralle an Innenseite des Vorderbeins suchen","Einige Hunde haben sie auch hinten","Häufiger schneiden als andere Krallen","Auf Einwachsung achten"],
|
||||||
|
"materialien":"Krallenschneider","haeufigkeit":"Alle 2–3 Wochen",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Wolfskralle ist DER häufigste Pflegefehler — vergiss sie nie!"},
|
||||||
|
# ── ZÄHNE ───────────────────────────────────────────────────────
|
||||||
|
{"id":"zahne_putzen", "titel":"Zähne täglich putzen","kat":"Zähne",
|
||||||
|
"beschreibung":"Zahnfleischentzündung betrifft 80% aller Hunde ab 3 Jahren — täglich putzen hilft.",
|
||||||
|
"schritte":["Hundezahnpasta auftragen (nie Menschenzahnpasta!)", "Finger einführen → kreisende Bewegung","Schrittweise zur Zahnbürste wechseln","Außenflächen priorisieren"],
|
||||||
|
"materialien":"Hundezahnpasta (Hühnergeschmack!), Fingerling oder Zahnbürste","haeufigkeit":"Täglich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Menschenzahnpasta ist giftig für Hunde — immer Hundezahnpasta!"},
|
||||||
|
{"id":"zahne_kauen", "titel":"Kauartikel für gesunde Zähne","kat":"Zähne",
|
||||||
|
"beschreibung":"Kauen reinigt Zähne mechanisch — das natürlichste Zahnpflegeprogramm.",
|
||||||
|
"schritte":["Roher Rinderknochen (nur roh!)", "Kauknochen aus Rinderhaut", "Dental-Chews (Greenies, Milkbone)", "Kauspielzeug aus Nylon"],
|
||||||
|
"materialien":"Kauknochen, Dental Chews","haeufigkeit":"Täglich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Gekochte Knochen splittern und sind lebensgefährlich — nur ROH!"},
|
||||||
|
{"id":"zahne_zahnstein", "titel":"Zahnstein erkennen & vorbeugen","kat":"Zähne",
|
||||||
|
"beschreibung":"Gelb-braune Beläge = Zahnstein → nur Tierarzt oder Ultraschall entfernt das.",
|
||||||
|
"schritte":["Regelmäßig Zähne kontrollieren (Lippen hochziehen)","Gelb = Plaque (wegputzbar), Braun = Zahnstein (Tierarzt)","Zahnpflege-Diät-Futter verwenden","Tierarzt-Kontrolle 1x/Jahr"],
|
||||||
|
"materialien":"Zahnpflegefutter, Zahngel","haeufigkeit":"Kontrolle wöchentlich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Zahnsteinentfernung in Narkose kostet 300–800€ — Vorsorge lohnt sich!"},
|
||||||
|
# ── OHREN ───────────────────────────────────────────────────────
|
||||||
|
{"id":"ohren_reinigen", "titel":"Ohren sauber halten","kat":"Ohren",
|
||||||
|
"beschreibung":"Dreckige Ohren = Ohrenentzündung — regelmäßige Reinigung schützt.",
|
||||||
|
"schritte":["Äußeren Gehörgang begutachten","Ohrenpflege-Lösung einträufeln","Mit weichem Tuch/Wattebausch reinigen","Nie mit Wattestäbchen tief rein"],
|
||||||
|
"materialien":"Ohrenreiniger für Hunde, Wattebausch","haeufigkeit":"Alle 2–4 Wochen",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Übler Geruch = Entzündung → sofort zum Tierarzt!"},
|
||||||
|
{"id":"ohren_haare", "titel":"Ohrenbehaarung trimmen","kat":"Ohren",
|
||||||
|
"beschreibung":"Dichte Ohrenbehaarung staut Luft — Otitis vorprogrammiert bei Hängeohrrassen.",
|
||||||
|
"schritte":["Haare im Außenohr vorsichtig mit Schere trimmen","Haare im inneren Ohrkanal: Trimmer oder Zupfen (nur wenn gelernt)", "Nach Schwimmen: Ohren immer trocknen","Beim Groomer: Ohren entnehmen lassen"],
|
||||||
|
"materialien":"Schere, Trimmer, Ohrenpfleger","haeufigkeit":"Monatlich",
|
||||||
|
"fell_typ":"alle","saison":"sommer","rassengruppe":"Hängeohrrassen","tipp":"Hängeohren (Cocker, Beagle) = höheres Risiko → häufiger kontrollieren."},
|
||||||
|
{"id":"ohren_check", "titel":"Wöchentlicher Ohren-Check","kat":"Ohren",
|
||||||
|
"beschreibung":"Früherkennung von Entzündungen spart Schmerzen und Kosten.",
|
||||||
|
"schritte":["Ohren hochklappen und reinschauen","Normal: hellrosa, leicht glänzend, kaum Geruch","Alarm: rot, geschwollen, dunkler Belag, Kratzen","Bei Auffälligkeiten: Tierarzt binnen 24h"],
|
||||||
|
"materialien":"Taschenlicht","haeufigkeit":"Wöchentlich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Hunde mit Allergien haben häufiger Ohrprobleme."},
|
||||||
|
# ── AUGEN ───────────────────────────────────────────────────────
|
||||||
|
{"id":"augen_reinigen", "titel":"Augenränder reinigen","kat":"Augen",
|
||||||
|
"beschreibung":"Tränenflecken und Schlieren — mit der richtigen Technik verschwinden sie.",
|
||||||
|
"schritte":["Feuchtes weiches Tuch oder Wattebausch","Von innen nach außen wischen","Nie auf die Hornhaut drücken","Augenreiniger für Hunde optional"],
|
||||||
|
"materialien":"Augenreiniger, Wattepads","haeufigkeit":"Täglich oder bei Bedarf",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Braune Tränenflecken bei Weißfell: spezielle Produkte oder Tierarzt."},
|
||||||
|
{"id":"augen_fell", "titel":"Fell vor Augen schneiden","kat":"Augen",
|
||||||
|
"beschreibung":"Fell im Gesicht kann Hornhaut kratzen — regelmäßiges Trimmen schützt.",
|
||||||
|
"schritte":["Abgerundete Schere verwenden","Fell von Augenrändern wegschneiden","Nie beim unruhigen Hund schneiden","Professioneller Groomer alle 4–6 Wochen"],
|
||||||
|
"materialien":"Abgerundete Schere","haeufigkeit":"Monatlich",
|
||||||
|
"fell_typ":"lang,lockig","saison":None,"rassengruppe":"Langhaarrassen","tipp":"Niemals spitze Schere in Augennähe — immer stumpf/abgerundet!"},
|
||||||
|
{"id":"augen_check", "titel":"Täglicher Augen-Check","kat":"Augen",
|
||||||
|
"beschreibung":"Augen erzählen viel über die Gesundheit — täglich kurz hinschauen.",
|
||||||
|
"schritte":["Klar und glänzend = gut","Trüb, gerötet, Ausfluss = Problem","Blinzeln oder Kratzen = Tierarzt","Licht mal mit Taschenlampe auf Pupillen"],
|
||||||
|
"materialien":None,"haeufigkeit":"Täglich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Plötzliche Trübung = Tierarzt HEUTE."},
|
||||||
|
# ── PFOTEN ──────────────────────────────────────────────────────
|
||||||
|
{"id":"pfoten_sommer", "titel":"Pfoten im Sommer schützen","kat":"Pfoten",
|
||||||
|
"beschreibung":"Asphalt im Sommer bis 70°C heiß — Pfotenballen verbrennen in Sekunden.",
|
||||||
|
"schritte":["Handtest: Hand 5 Sek auf Asphalt → zu heiß wenn unerträglich","Morgens/abends spazieren","Pfotenbalsam schützt und pflegt","Hundeschuhe für empfindliche Hunde"],
|
||||||
|
"materialien":"Pfotenbalsam, Hundeschuhe","haeufigkeit":"Täglich im Sommer",
|
||||||
|
"fell_typ":"alle","saison":"sommer","tipp":"Regel: Wenn du barfuß nicht draufstehen kannst, dein Hund auch nicht!"},
|
||||||
|
{"id":"pfoten_winter", "titel":"Pfoten im Winter / Streusalz","kat":"Pfoten",
|
||||||
|
"beschreibung":"Streusalz ist ätzend — rissige Ballen und Vergiftung durch Ablecken.",
|
||||||
|
"schritte":["Vor dem Spaziergang: Pfotenbalsam auftragen","Nach dem Spaziergang: Pfoten mit lauwarmem Wasser abspülen","Zwischen Zehen prüfen: kein Eis","Hundeschuhe bei viel Salz"],
|
||||||
|
"materialien":"Pfotenbalsam, Wasser, kleines Becken","haeufigkeit":"Täglich im Winter",
|
||||||
|
"fell_typ":"alle","saison":"winter","tipp":"Musher's Secret ist der Pfoten-Klassiker für den Winter."},
|
||||||
|
{"id":"pfoten_haare", "titel":"Pfotenhaare trimmen","kat":"Pfoten",
|
||||||
|
"beschreibung":"Zu lange Haare zwischen den Zehen = Rutschgefahr und Schmutzfänger.",
|
||||||
|
"schritte":["Haare zwischen Zehenballen mit Schere oder Trimmer","Bündig mit Ballenoberfläche kürzen","Nicht zu kurz — Schutzfunktion erhalten","Auf Wunden und Einschüsse achten"],
|
||||||
|
"materialien":"Abgerundete Schere oder Trimmer","haeufigkeit":"Alle 3–4 Wochen",
|
||||||
|
"fell_typ":"lang,lockig","saison":None,"tipp":"Rutschiger Parkett + lange Pfotenhaare = Kreuzbandriss-Risiko!"},
|
||||||
|
{"id":"pfoten_ballen", "titel":"Pfotenballen pflegen","kat":"Pfoten",
|
||||||
|
"beschreibung":"Rissige Ballen sind schmerzhaft — regelmäßige Pflege beugt vor.",
|
||||||
|
"schritte":["Ballen wöchentlich kontrollieren","Risse eincremen mit Pfotenbalsam","Tiefe Risse: Verbandsmull + Tierarzt","Im Sommer nach Sand/Strand extra eincremen"],
|
||||||
|
"materialien":"Pfotenbalsam, Wattepads","haeufigkeit":"Wöchentlich (täglich im Winter/Sommer)",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Bienen-Propolis-Balsam hat heilende Wirkung auf rissige Ballen."},
|
||||||
|
{"id":"pfoten_reinigen", "titel":"Pfoten nach dem Spaziergang reinigen","kat":"Pfoten",
|
||||||
|
"beschreibung":"Dreck, Giftstoffe und Parasiten kommen mit rein — kurze Routine macht Unterschied.",
|
||||||
|
"schritte":["Kleines Becken mit lauwarmem Wasser","Jede Pfote kurz eintauchen und abwischen","Zwischen Zehen kontrollieren","Abtrocknen — feuchte Pfoten = Pilzgefahr"],
|
||||||
|
"materialien":"Pfoten-Reinigungsbecher, Handtuch","haeufigkeit":"Nach jedem Spaziergang",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Pfotenwasch-Becher (Dexas Mudbuster) ist der Gamechanger!"},
|
||||||
|
# ── PARASITEN ───────────────────────────────────────────────────
|
||||||
|
{"id":"parasiten_zecken_check","titel":"Zecken-Check nach Spaziergang","kat":"Parasiten",
|
||||||
|
"beschreibung":"Zecken übertragen Borreliose und FSME — täglicher Check rettet Leben.",
|
||||||
|
"schritte":["Kopf: um Augen, Ohren, Hals","Achseln und Leiste","Zwischen Zehen","Schwanzwurzel und Analbereich","Fell gegen Strich durchkämmen"],
|
||||||
|
"materialien":"Feine Zeckenzange oder Zeckenhaken","haeufigkeit":"Nach jedem Spaziergang im Gras",
|
||||||
|
"fell_typ":"alle","saison":"fruehling,sommer,herbst","tipp":"Zecken erst nach 24h übertragen meist Erreger — täglich reicht!"},
|
||||||
|
{"id":"parasiten_zecken_entfernen","titel":"Zecke richtig entfernen","kat":"Parasiten",
|
||||||
|
"beschreibung":"Falsch entfernt = Erkrankungsrisiko erhöht — diese Technik ist richtig.",
|
||||||
|
"schritte":["Zeckenzange nah an Haut ansetzen","Gerade rausziehen, nicht drehen oder quetschen","Kein Öl, Klebstoff oder Nagellack","Bissstelle 2 Wochen beobachten","Bei Rötung: Tierarzt"],
|
||||||
|
"materialien":"Zeckenzange/-haken","haeufigkeit":"Sofort wenn Zecke gefunden",
|
||||||
|
"fell_typ":"alle","saison":"fruehling,sommer,herbst","tipp":"Fotografiere Zecke und Bissstelle zur Dokumentation."},
|
||||||
|
{"id":"parasiten_prophylaxe","titel":"Zecken- und Floh-Prophylaxe","kat":"Parasiten",
|
||||||
|
"beschreibung":"Spot-on, Tablette oder Halsband — die Vor- und Nachteile.",
|
||||||
|
"schritte":["Spot-on: monatlich auf Nacken, zuverlässig","Tabletten (NexGard, Bravecto): bequem, für Wasserhunde","Zeckenhalsband (Seresto): 8 Monate Schutz","Natürlich: Kokosöl, Schwarzkümmelöl (schwächerer Schutz)"],
|
||||||
|
"materialien":"Tierarzt-Empfehlung","haeufigkeit":"Je nach Produkt",
|
||||||
|
"fell_typ":"alle","saison":"fruehling,sommer,herbst","tipp":"Produkte immer vom Tierarzt — manche Katzen-Präparate töten Hunde!"},
|
||||||
|
{"id":"parasiten_floh", "titel":"Floh-Befall erkennen und behandeln","kat":"Parasiten",
|
||||||
|
"beschreibung":"Flöhe sieht man selten — aber Flohkot ist eindeutig.",
|
||||||
|
"schritte":["Weißes Tuch unter Hund, kämmen → schwarze Punkte = Flohkot","Feuchtes Küchentuch: Flohkot wird rötlich-braun","Hund + Wohnung gleichzeitig behandeln","Flohpuder und -spray für Umgebung"],
|
||||||
|
"materialien":"Floh-Kamm, Flohspray, Tierarzt-Präparat","haeufigkeit":"Bei Verdacht + Prophylaxe",
|
||||||
|
"fell_typ":"alle","saison":"sommer","tipp":"80% der Flöhe leben in der Wohnung, nicht am Hund!"},
|
||||||
|
{"id":"parasiten_wurm", "titel":"Wurm-Prophylaxe","kat":"Parasiten",
|
||||||
|
"beschreibung":"Würmer übertragen sich auf Menschen — regelmäßige Behandlung schützt die Familie.",
|
||||||
|
"schritte":["Wurmkur alle 3–6 Monate (je nach Lebensweise)","Kot-Untersuchung beim Tierarzt","Nach Zeckenbiss: auf Herzwurm testen (Reisehunde)","Hände waschen nach Hundekontakt"],
|
||||||
|
"materialien":"Wurmmittel vom Tierarzt","haeufigkeit":"Alle 3–6 Monate",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Rohfleisch-Hunde öfter entwurmen — rohes Fleisch überträgt Parasiten."},
|
||||||
|
# ── SAISONAL ────────────────────────────────────────────────────
|
||||||
|
{"id":"saison_hitze", "titel":"Pflege im Sommer / Hitzeschutz","kat":"Saisonal",
|
||||||
|
"beschreibung":"Hunde schwitzen durch Pfoten und Hecheln — Überhitzung ist lebensgefährlich.",
|
||||||
|
"schritte":["Spaziergänge in früh/spät","Immer Wasser dabei","Nasses Tuch auf Bauch kühlt schnell","Niemals im Auto lassen"],
|
||||||
|
"materialien":"Kühlmatte, Kühlweste, Wassernapf","haeufigkeit":"Täglich im Sommer",
|
||||||
|
"fell_typ":"alle","saison":"sommer","tipp":"Kurznasige Rassen (Mops, Bulldogge): extreme Hitzegefahr!"},
|
||||||
|
{"id":"saison_scheren", "titel":"Sommerhaarschnitt — ja oder nein?","kat":"Saisonal",
|
||||||
|
"beschreibung":"Doppelfell scheren schadet mehr als es nützt — Missverständnis aufklären!",
|
||||||
|
"schritte":["Doppelfell (Husky, Retriever): NICHT scheren — kühlt selbst","Einfaches langes Fell: kürzen ok","Stattdessen: intensiv ausbürsten, Unterwolle entfernen","Scheren nur wenn verfilzt oder Veterinär rät"],
|
||||||
|
"materialien":"Bürste, Undercoat-Rake","haeufigkeit":"Saisonal",
|
||||||
|
"fell_typ":"doppel,lang","saison":"sommer","tipp":"Geschorenes Doppelfell kann sich falsch nachwachsen — Textur-Veränderung dauerhaft."},
|
||||||
|
{"id":"saison_winter_kalt","titel":"Winterpflege für empfindliche Hunde","kat":"Saisonal",
|
||||||
|
"beschreibung":"Kleine, kurzhaarige und ältere Hunde frieren — Schutz ist Pflicht.",
|
||||||
|
"schritte":["Hundemantel für Kurzhaar unter 5°C","Nach draußen: nie nass raus","Fell nach Schnee/Regen sofort trocknen","Alters-Check: Gelenke im Winter öfter warm halten"],
|
||||||
|
"materialien":"Hundemantel, Pfotenschutz","haeufigkeit":"Täglich im Winter",
|
||||||
|
"fell_typ":"kurz","saison":"winter","rassengruppe":"Kurzhaarrassen","tipp":"Chihuahua und Co. im Winter wirklich mit Mantel — kein Modethema!"},
|
||||||
|
{"id":"saison_pollen", "titel":"Allergiezeit / Pollenflug","kat":"Saisonal",
|
||||||
|
"beschreibung":"Hunde haben Pollenallergien — Jucken, Pfotenlecken, Ohrenprobleme.",
|
||||||
|
"schritte":["Nach Spaziergang Fell und Pfoten abwischen","Pollen-App nutzen: Hochphasen meiden","Tierarzt: Antihistaminika für Hunde","Hypoallergenes Shampoo verwenden"],
|
||||||
|
"materialien":"Pollen-App, feuchte Tücher","haeufigkeit":"Täglich im Frühling",
|
||||||
|
"fell_typ":"alle","saison":"fruehling","tipp":"Pollenallergie kann Hautprobleme verursachen — oft verwechselt mit Futtermittelallergie."},
|
||||||
|
{"id":"saison_fell_wechsel","titel":"Fellwechsel-Saison meistern","kat":"Saisonal",
|
||||||
|
"beschreibung":"2x jährlich Haare-Chaos — mit der richtigen Routine überleben alle.",
|
||||||
|
"schritte":["Täglich bürsten während Fellwechsel","Furminator 2x/Woche (nicht öfter)","Lüfter hilft lose Haare wegzublasen","Professionelles Grooming buchen"],
|
||||||
|
"materialien":"Furminator, Staubsauger mit Tierhaar-Aufsatz","haeufigkeit":"Täglich März–Mai, Sep–Nov",
|
||||||
|
"fell_typ":"doppel","saison":"fruehling,herbst","tipp":"Ein 10-Minuten-Bürstsession täglich ersetzt stundenlange Staubsauger-Session."},
|
||||||
|
# ── GESUNDHEITSVORSORGE ──────────────────────────────────────────
|
||||||
|
{"id":"gesund_impfung", "titel":"Impfkalender verstehen","kat":"Gesundheitsvorsorge",
|
||||||
|
"beschreibung":"Welche Impfungen wirklich nötig sind — Kern- vs. Non-Core-Impfungen.",
|
||||||
|
"schritte":["Kernimpfungen: Staupe, Parvo, Leptospirose jährlich/alle 3 Jahre","Non-Core je nach Lebensweise: Tollwut, Zwinger-Husten","Impfpass immer aktuell halten","Jährlicher Tierarztbesuch zum Check"],
|
||||||
|
"materialien":"Impfpass","haeufigkeit":"Jährlich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Überimpfung gibt es — Titer-Test statt Routine-Booster möglich."},
|
||||||
|
{"id":"gesund_vorsorge", "titel":"Jährlicher Gesundheitscheck","kat":"Gesundheitsvorsorge",
|
||||||
|
"beschreibung":"Blutbild + körperliche Untersuchung einmal jährlich — Früherkennung rettet Leben.",
|
||||||
|
"schritte":["Einmal jährlich Tierarzt auch ohne Anlass","Ab 7 Jahren: halbjährlich","Blutbild gibt Einblick in Organfunktion","Gewicht kontrollieren"],
|
||||||
|
"materialien":"Tierarzt-Termin","haeufigkeit":"Jährlich (ab 7 J. halbjährlich)",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Senior-Hunde ab 7 Jahre: halbjährlicher Check lohnt sich sehr."},
|
||||||
|
{"id":"gesund_gewicht", "titel":"Idealgewicht halten","kat":"Gesundheitsvorsorge",
|
||||||
|
"beschreibung":"70% aller Hunde in Deutschland sind übergewichtig — Rippen müssen fühlbar sein.",
|
||||||
|
"schritte":["Rippentest: leicht zu fühlen, nicht zu sehen → Idealgewicht","Taille von oben erkennbar","Leckerlis: max. 10% des Tagesbedarfs","Futtermenge nach Aktivität anpassen"],
|
||||||
|
"materialien":"Küchenwaage für Futter","haeufigkeit":"Wöchentliche Sichtkontrolle",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"1 kg Übergewicht beim 5-kg-Hund = 20 kg beim Menschen!"},
|
||||||
|
{"id":"gesund_zahnkontrolle","titel":"Monatliche Mundhöhlen-Kontrolle","kat":"Gesundheitsvorsorge",
|
||||||
|
"beschreibung":"Zahn- und Zahnfleischprobleme schmerzen — und Hunde zeigen das selten.",
|
||||||
|
"schritte":["Lippen hochziehen: Zähne und Zahnfleisch sehen","Zahnfleisch: rosa, glatt = gut; rot, geschwollen = Problem","Zähne: weiß/crème = ok; braun = Zahnstein","Mundgeruch plötzlich stark = Tierarzt"],
|
||||||
|
"materialien":None,"haeufigkeit":"Monatlich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Hunde zeigen Zahnschmerzen durch schlechteres Fressen, weniger Spielen."},
|
||||||
|
{"id":"gesund_körpercheck","titel":"Wöchentlicher Körper-Check","kat":"Gesundheitsvorsorge",
|
||||||
|
"beschreibung":"5 Minuten wöchentlich — Beulen, Wunden, Veränderungen früh erkennen.",
|
||||||
|
"schritte":["Kopf bis Schwanz abtasten","Lymphknoten fühlen (Kieferwinkel, Hals, Achsel, Leiste)","Auf Schmerz-Reaktionen achten","Alles unbekannte: Tierarzt"],
|
||||||
|
"materialien":"Gutes Licht","haeufigkeit":"Wöchentlich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Du kennst deinen Hund am besten — vertrau deinem Gefühl."},
|
||||||
|
{"id":"gesund_senior", "titel":"Pflege für Senior-Hunde","kat":"Gesundheitsvorsorge",
|
||||||
|
"beschreibung":"Ab 7–8 Jahren ändern sich Bedürfnisse — angepasste Pflege macht Unterschied.",
|
||||||
|
"schritte":["Kürzere, öftere Spaziergänge statt langer Touren","Orthopädische Schlafmatte","Gelenk-Supplemente (Glucosamin, Omega-3)","Mehr Körperchecks, öftere Tierarztbesuche"],
|
||||||
|
"materialien":"Orthopädie-Schlafmatte, Gelenk-Supplements","haeufigkeit":"Täglich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Senior-Hund = mehr Pflege, mehr Liebe, mehr Tierarzt — so einfach."},
|
||||||
|
# ── WELPEN-PFLEGE ────────────────────────────────────────────────
|
||||||
|
{"id":"welpe_erste_pflege","titel":"Erste Pflege-Einführung beim Welpen","kat":"Welpen-Pflege",
|
||||||
|
"beschreibung":"Was du jetzt mit dem Welpen übst, macht das spätere Leben einfacher.",
|
||||||
|
"schritte":["Täglich Pfoten, Ohren, Mund anfassen — immer positiv", "Kämmen einführen mit weicher Bürste","Nagel-Feile zeigen: noch nicht benutzen, nur zeigen","Alles mit Leckerli verbinden"],
|
||||||
|
"materialien":"Weiche Bürste, Leckerlis","haeufigkeit":"Täglich",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Was du im ersten Jahr tust, sparst du im Rest des Lebens."},
|
||||||
|
{"id":"welpe_badegewöhnung","titel":"Welpe ans Baden gewöhnen","kat":"Welpen-Pflege",
|
||||||
|
"beschreibung":"Erste Baderr-Erfahrung prägt lebenslang — positiv starten!",
|
||||||
|
"schritte":["Leeres Becken mit Leckerlis auskleiden","Wasser einlaufen lassen: Welpe schaut zu","Pfoten einweichen lassen","Erstes Bad sehr kurz (2 Min), viel Lob"],
|
||||||
|
"materialien":"Flache Wanne, mildes Welpen-Shampoo","haeufigkeit":"Alle 6–8 Wochen",
|
||||||
|
"fell_typ":"alle","saison":None,"tipp":"Wassertemperatur: wie für ein Baby — lauwarm."},
|
||||||
|
]
|
||||||
|
|
||||||
|
_PROMPT_PFLEGE = '''\
|
||||||
|
Erstelle einen Social-Media-Post über diesen Hunde-Pflegetipp für Ban Yaro (banyaro.app).
|
||||||
|
|
||||||
|
Pflegetipp: {titel}
|
||||||
|
Kategorie: {kat}
|
||||||
|
Beschreibung: {beschreibung}
|
||||||
|
Schritte: {schritte}
|
||||||
|
Materialien: {materialien}
|
||||||
|
Häufigkeit: {haeufigkeit}
|
||||||
|
Profi-Tipp: {tipp}
|
||||||
|
{rasse_kontext}
|
||||||
|
|
||||||
|
Antworte NUR als JSON:
|
||||||
|
{{
|
||||||
|
"caption": "Post-Text mit Emojis, lehrreich aber locker, max 700 Zeichen. Startet mit einem Fakten-Hook.",
|
||||||
|
"hashtags": "10 Hashtags kommagetrennt: hundepflege, hundegesundheit, {kat_lower}, banyaro + passende",
|
||||||
|
"hook": "Erste Zeile die sofort Aufmerksamkeit fängt (Fakt oder Frage)",
|
||||||
|
"cta": "Frage ans Publikum passend zum Thema",
|
||||||
|
"visual_brief": "Was man fotografieren/filmen sollte: konkretes Bild das den Tipp zeigt",
|
||||||
|
"canva_notes": "Infografik-Idee: z.B. Schritt-für-Schritt mit Icons",
|
||||||
|
"unsplash_query": "2–3 englische Suchbegriffe für passendes Stockfoto",
|
||||||
|
"ai_score": <1-5>,
|
||||||
|
"category": "pflege",
|
||||||
|
"coaching": "Warum ist dieser Pflegetipp wichtig für die Zielgruppe? Wie macht man das als Video? (1–2 Sätze)"
|
||||||
|
}}
|
||||||
|
'''
|
||||||
|
|
||||||
_TRAINING_STILE = [
|
_TRAINING_STILE = [
|
||||||
"tutorial", # "So geht das:"
|
"tutorial", # "So geht das:"
|
||||||
"community", # "Sind grad dran das zu lernen"
|
"community", # "Sind grad dran das zu lernen"
|
||||||
|
|
@ -598,6 +856,7 @@ class StatusUpdate(BaseModel):
|
||||||
scheduled_at: Optional[str] = None
|
scheduled_at: Optional[str] = None
|
||||||
published_at: Optional[str] = None
|
published_at: Optional[str] = None
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
post_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
def _used_topics(limit: int = 30) -> str:
|
def _used_topics(limit: int = 30) -> str:
|
||||||
|
|
@ -932,8 +1191,23 @@ def _seed_exercises():
|
||||||
u.get("tipp")),
|
u.get("tipp")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _seed_pflege():
|
||||||
|
with db() as conn:
|
||||||
|
for p in _PFLEGE_TIPPS:
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT OR IGNORE INTO pflege_tipps
|
||||||
|
(tipp_id, titel, kategorie, beschreibung, schritte,
|
||||||
|
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")),
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_seed_exercises()
|
_seed_exercises()
|
||||||
|
_seed_pflege()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -1210,6 +1484,118 @@ async def get_stats(user=Depends(require_social_media)):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /api/social/pflege-tipp — Pflegetipp generieren (allg. oder rassenspezifisch)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.post("/pflege-tipp")
|
||||||
|
async def pflege_tipp(breed_id: Optional[int] = None, user=Depends(require_social_media)):
|
||||||
|
with db() as conn:
|
||||||
|
used_ids = {r["exercise_id"] for r in conn.execute(
|
||||||
|
"SELECT exercise_id FROM social_content WHERE exercise_id IS NOT NULL"
|
||||||
|
).fetchall()}
|
||||||
|
|
||||||
|
# Rassenspezifische Tipps bevorzugen wenn breed_id angegeben
|
||||||
|
rasse = None
|
||||||
|
if breed_id:
|
||||||
|
rasse = conn.execute(
|
||||||
|
"SELECT name, groesse, beschreibung, herkunft FROM wiki_rassen WHERE id=?",
|
||||||
|
(breed_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
tipps = conn.execute(
|
||||||
|
"SELECT * FROM pflege_tipps ORDER BY RANDOM()"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
# Noch nicht verwendete bevorzugen
|
||||||
|
unused = [t for t in tipps if t["tipp_id"] not in used_ids]
|
||||||
|
pool = unused if unused else list(tipps)
|
||||||
|
|
||||||
|
# Bei Rasse: Fell-Typ-Filter wenn möglich
|
||||||
|
if rasse and rasse["groesse"]:
|
||||||
|
fell_filter = "kurz" if rasse["groesse"] in ("klein", "mittel") else "lang"
|
||||||
|
relevant = [t for t in pool if not t["fell_typ"] or t["fell_typ"] == "alle"
|
||||||
|
or fell_filter in (t["fell_typ"] or "")]
|
||||||
|
if relevant:
|
||||||
|
pool = relevant
|
||||||
|
|
||||||
|
if not pool:
|
||||||
|
raise HTTPException(404, "Keine Pflegetipps gefunden.")
|
||||||
|
|
||||||
|
t = dict(pool[0])
|
||||||
|
schritte_list = json.loads(t["schritte"] or "[]")
|
||||||
|
schritte_text = "\n".join(f" {i+1}. {s}" for i, s in enumerate(schritte_list[:5]))
|
||||||
|
|
||||||
|
rasse_kontext = ""
|
||||||
|
if rasse:
|
||||||
|
rasse_kontext = (
|
||||||
|
f"\nRassenspezifisch für: {rasse['name']} "
|
||||||
|
f"(Größe: {rasse['groesse'] or 'unbekannt'})\n"
|
||||||
|
"Passe den Post gezielt an diese Rasse an."
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = _PROMPT_PFLEGE.format(
|
||||||
|
titel=t["titel"],
|
||||||
|
kat=t["kategorie"],
|
||||||
|
beschreibung=t["beschreibung"] or t["titel"],
|
||||||
|
schritte=schritte_text,
|
||||||
|
materialien=t["materialien"] or "Standard Pflegeutensilien",
|
||||||
|
haeufigkeit=t["haeufigkeit"] or "Nach Bedarf",
|
||||||
|
tipp=t["tipp"] or "",
|
||||||
|
rasse_kontext=rasse_kontext,
|
||||||
|
kat_lower=t["kategorie"].lower().replace(" ", "").replace("-", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = await _ki_complete(prompt)
|
||||||
|
data = _parse_json(raw)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, f"KI-Fehler: {e}")
|
||||||
|
|
||||||
|
topic = f"Pflegetipp: {t['titel']}"
|
||||||
|
if rasse:
|
||||||
|
topic += f" ({rasse['name']})"
|
||||||
|
|
||||||
|
with db() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
"""INSERT INTO social_content
|
||||||
|
(created_by, platform, format, topic, caption, hashtags,
|
||||||
|
visual_brief, canva_notes, hook, cta, unsplash_query,
|
||||||
|
ai_score, source, coaching, category, exercise_id)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
|
(user["id"], "both", "post", topic,
|
||||||
|
data.get("caption"), data.get("hashtags"),
|
||||||
|
data.get("visual_brief"), data.get("canva_notes"),
|
||||||
|
data.get("hook"), data.get("cta"), data.get("unsplash_query"),
|
||||||
|
data.get("ai_score"), "generated", data.get("coaching"),
|
||||||
|
"pflege", t["tipp_id"]),
|
||||||
|
)
|
||||||
|
entry_id = cur.lastrowid
|
||||||
|
|
||||||
|
with db() as conn:
|
||||||
|
row = conn.execute("SELECT * FROM social_content WHERE id=?", (entry_id,)).fetchone()
|
||||||
|
result = dict(row)
|
||||||
|
result["pflege_titel"] = t["titel"]
|
||||||
|
result["pflege_kat"] = t["kategorie"]
|
||||||
|
result["rasse_name"] = rasse["name"] if rasse else None
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/social/pflege-kategorien — Statistik Pflegetipps
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/pflege-kategorien")
|
||||||
|
async def pflege_kategorien(user=Depends(require_social_media)):
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT p.kategorie, COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN sc.exercise_id IS NOT NULL THEN 1 ELSE 0 END) as used
|
||||||
|
FROM pflege_tipps p
|
||||||
|
LEFT JOIN social_content sc ON sc.exercise_id = p.tipp_id
|
||||||
|
GROUP BY p.kategorie ORDER BY p.kategorie"""
|
||||||
|
).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# POST /api/social/media — Medien-Upload (Foto/Video)
|
# POST /api/social/media — Medien-Upload (Foto/Video)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '337'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '343'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
|
|
||||||
const App = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ window.Page_dog_profile = (() => {
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
<div id="dp-skills" style="margin-bottom:var(--space-5);text-align:left"></div>
|
<div id="dp-skills" style="margin-bottom:var(--space-5);text-align:left"></div>
|
||||||
|
<div id="dp-pflege" style="margin-bottom:var(--space-5);text-align:left"></div>
|
||||||
|
|
||||||
${dog.is_public ? `
|
${dog.is_public ? `
|
||||||
<div style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light);
|
<div style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light);
|
||||||
|
|
@ -220,6 +221,9 @@ window.Page_dog_profile = (() => {
|
||||||
// Skills laden
|
// Skills laden
|
||||||
_loadSkills(dog);
|
_loadSkills(dog);
|
||||||
|
|
||||||
|
// Pflegetipps laden
|
||||||
|
_loadPflegeTipps(dog);
|
||||||
|
|
||||||
// Sitter-Zugang laden (nur für Besitzer)
|
// Sitter-Zugang laden (nur für Besitzer)
|
||||||
if (dog.user_id === _appState.user?.id) {
|
if (dog.user_id === _appState.user?.id) {
|
||||||
_loadSittingAccess(dog.id);
|
_loadSittingAccess(dog.id);
|
||||||
|
|
@ -324,6 +328,123 @@ window.Page_dog_profile = (() => {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// PFLEGETIPPS
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
async function _loadPflegeTipps(dog) {
|
||||||
|
const el = document.getElementById('dp-pflege');
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await API.get(`/dogs/${dog.id}/pflege`);
|
||||||
|
} catch { return; }
|
||||||
|
|
||||||
|
if (!data?.tipps?.length) return;
|
||||||
|
|
||||||
|
const t = data.tipp_des_tages;
|
||||||
|
const kat_icons = {
|
||||||
|
'Fell':'✂️','Krallen':'💅','Zähne':'🦷','Ohren':'👂',
|
||||||
|
'Augen':'👁','Pfoten':'🐾','Parasiten':'🦟',
|
||||||
|
'Saisonal':'🌸','Gesundheitsvorsorge':'❤️','Welpen-Pflege':'🐶',
|
||||||
|
};
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="card" style="padding:var(--space-4)">
|
||||||
|
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
|
||||||
|
<span style="font-size:1.1em">🛁</span>
|
||||||
|
<span style="font-size:var(--text-sm);font-weight:600">
|
||||||
|
Pflegetipps${data.rasse_name ? ` für ${_esc(data.rasse_name)}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${t ? `
|
||||||
|
<!-- Tipp des Tages -->
|
||||||
|
<div style="background:var(--c-surface-2);border-radius:10px;padding:12px;
|
||||||
|
margin-bottom:var(--space-3);border-left:3px solid #a78bfa">
|
||||||
|
<div style="font-size:10px;font-weight:700;color:#a78bfa;text-transform:uppercase;
|
||||||
|
letter-spacing:.5px;margin-bottom:4px">
|
||||||
|
${t.saisonal_aktuell ? '🌸 Aktuell & Saisonal' : '💡 Tipp des Tages'}
|
||||||
|
</div>
|
||||||
|
<div style="font-weight:600;font-size:var(--text-sm);margin-bottom:4px">
|
||||||
|
${kat_icons[t.kategorie]||'🐾'} ${_esc(t.titel)}
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:var(--c-text-secondary);margin-bottom:8px;
|
||||||
|
line-height:1.5">${_esc(t.beschreibung||'')}</div>
|
||||||
|
${t.haeufigkeit ? `<div style="font-size:11px;color:var(--c-text-muted)">
|
||||||
|
🔄 ${_esc(t.haeufigkeit)}</div>` : ''}
|
||||||
|
${t.materialien ? `<div style="font-size:11px;color:var(--c-text-muted)">
|
||||||
|
🛒 ${_esc(t.materialien)}</div>` : ''}
|
||||||
|
${t.schritte?.length ? `
|
||||||
|
<details style="margin-top:8px">
|
||||||
|
<summary style="font-size:12px;cursor:pointer;color:var(--c-primary);
|
||||||
|
font-weight:600">Anleitung anzeigen</summary>
|
||||||
|
<ol style="margin:8px 0 0 16px;padding:0;font-size:12px;
|
||||||
|
color:var(--c-text);line-height:1.6">
|
||||||
|
${t.schritte.map(s=>`<li style="margin-bottom:3px">${_esc(s)}</li>`).join('')}
|
||||||
|
</ol>
|
||||||
|
${t.tipp ? `<div style="margin-top:8px;font-size:11px;color:#a78bfa;
|
||||||
|
font-style:italic">💜 ${_esc(t.tipp)}</div>` : ''}
|
||||||
|
</details>` : ''}
|
||||||
|
</div>` : ''}
|
||||||
|
|
||||||
|
<!-- Alle Tipps Button -->
|
||||||
|
<button id="dp-pflege-alle" class="btn btn-secondary btn-sm"
|
||||||
|
style="width:100%;font-size:12px">
|
||||||
|
Alle ${data.tipps.length} Pflegetipps anzeigen
|
||||||
|
</button>
|
||||||
|
<div id="dp-pflege-liste" style="display:none;margin-top:var(--space-3)">
|
||||||
|
${data.kategorien.map(kat => {
|
||||||
|
const katTipps = data.tipps.filter(t=>t.kategorie===kat);
|
||||||
|
return `
|
||||||
|
<div style="margin-bottom:var(--space-3)">
|
||||||
|
<div style="font-size:11px;font-weight:700;color:var(--c-text-muted);
|
||||||
|
text-transform:uppercase;margin-bottom:8px">
|
||||||
|
${kat_icons[kat]||'🐾'} ${_esc(kat)}</div>
|
||||||
|
${katTipps.map(tip => `
|
||||||
|
<details style="background:var(--c-surface-2);border-radius:8px;
|
||||||
|
padding:10px;margin-bottom:6px">
|
||||||
|
<summary style="font-size:var(--text-sm);font-weight:600;cursor:pointer;
|
||||||
|
list-style:none;display:flex;justify-content:space-between;
|
||||||
|
align-items:center">
|
||||||
|
${_esc(tip.titel)}
|
||||||
|
${tip.saisonal_aktuell ? '<span style="font-size:10px;color:#10b981">● Aktuell</span>' : ''}
|
||||||
|
</summary>
|
||||||
|
<div style="margin-top:8px;font-size:12px;color:var(--c-text-secondary);
|
||||||
|
line-height:1.5">${_esc(tip.beschreibung||'')}</div>
|
||||||
|
${tip.haeufigkeit ? `<div style="font-size:11px;color:var(--c-text-muted);
|
||||||
|
margin-top:4px">🔄 ${_esc(tip.haeufigkeit)}</div>` : ''}
|
||||||
|
${tip.schritte?.length ? `
|
||||||
|
<ol style="margin:8px 0 0 16px;padding:0;font-size:12px;line-height:1.6">
|
||||||
|
${tip.schritte.map(s=>`<li style="margin-bottom:3px">${_esc(s)}</li>`).join('')}
|
||||||
|
</ol>` : ''}
|
||||||
|
${tip.tipp ? `<div style="margin-top:6px;font-size:11px;color:#a78bfa;
|
||||||
|
font-style:italic">💜 ${_esc(tip.tipp)}</div>` : ''}
|
||||||
|
</details>`).join('')}
|
||||||
|
</div>`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
el.querySelector('#dp-pflege-alle')?.addEventListener('click', e => {
|
||||||
|
const liste = el.querySelector('#dp-pflege-liste');
|
||||||
|
const btn = e.currentTarget;
|
||||||
|
if (liste.style.display === 'none') {
|
||||||
|
liste.style.display = '';
|
||||||
|
btn.textContent = 'Pflegetipps einklappen ▲';
|
||||||
|
} else {
|
||||||
|
liste.style.display = 'none';
|
||||||
|
btn.textContent = `Alle ${data.tipps.length} Pflegetipps anzeigen`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _esc(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<')
|
||||||
|
.replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// SITTER-ZUGANG
|
// SITTER-ZUGANG
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -788,11 +909,21 @@ window.Page_dog_profile = (() => {
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
Rasse
|
Rasse
|
||||||
<span style="color:var(--c-text-secondary)">(optional)</span>
|
<span style="color:var(--c-text-secondary)">(optional)</span>
|
||||||
${UI.help('Die Rasse wird für Rasseninformationen und Statistiken verwendet.')}
|
${UI.help('Verknüpfe deine Rasse mit unserem Wiki für personalisierte Pflegetipps.')}
|
||||||
</label>
|
</label>
|
||||||
<input class="form-control" type="text" name="rasse"
|
<input class="form-control" type="text" name="rasse"
|
||||||
|
id="dp-rasse-input"
|
||||||
value="${_esc(dog?.rasse || '')}"
|
value="${_esc(dog?.rasse || '')}"
|
||||||
|
list="dp-rasse-list"
|
||||||
|
autocomplete="off"
|
||||||
placeholder="z. B. Mischling, Golden Retriever…">
|
placeholder="z. B. Mischling, Golden Retriever…">
|
||||||
|
<datalist id="dp-rasse-list"></datalist>
|
||||||
|
<input type="hidden" name="rasse_id" id="dp-rasse-id"
|
||||||
|
value="${dog?.rasse_id || ''}">
|
||||||
|
<div id="dp-rasse-match" style="display:none;margin-top:4px;font-size:11px;
|
||||||
|
color:var(--c-success);font-weight:600">
|
||||||
|
✓ Mit Wiki verknüpft — Pflegetipps werden auf diese Rasse angepasst
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||||
|
|
@ -882,6 +1013,45 @@ window.Page_dog_profile = (() => {
|
||||||
const form = document.getElementById('dp-form');
|
const form = document.getElementById('dp-form');
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
|
|
||||||
|
// Rassen-Autocomplete aus Wiki laden
|
||||||
|
let _wikiBreeds = [];
|
||||||
|
API.get('/wiki/rassen?limit=1000&offset=0').then(data => {
|
||||||
|
_wikiBreeds = data.rassen || [];
|
||||||
|
const list = document.getElementById('dp-rasse-list');
|
||||||
|
if (list) {
|
||||||
|
list.innerHTML = _wikiBreeds.map(r =>
|
||||||
|
`<option value="${r.name.replace(/"/g,'"')}" data-id="${r.id}">`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
// Vorhandene Rasse: Match prüfen und Badge zeigen
|
||||||
|
const rasseInput = document.getElementById('dp-rasse-input');
|
||||||
|
const rasseIdInput = document.getElementById('dp-rasse-id');
|
||||||
|
const matchBadge = document.getElementById('dp-rasse-match');
|
||||||
|
if (rasseInput?.value) {
|
||||||
|
const match = _wikiBreeds.find(r =>
|
||||||
|
r.name.toLowerCase() === rasseInput.value.toLowerCase());
|
||||||
|
if (match && matchBadge) {
|
||||||
|
if (!rasseIdInput.value) rasseIdInput.value = match.id;
|
||||||
|
matchBadge.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
// Rassen-Input: bei Änderung ID nachschlagen
|
||||||
|
document.getElementById('dp-rasse-input')?.addEventListener('input', e => {
|
||||||
|
const val = e.target.value.trim().toLowerCase();
|
||||||
|
const rasseIdInput = document.getElementById('dp-rasse-id');
|
||||||
|
const matchBadge = document.getElementById('dp-rasse-match');
|
||||||
|
const match = _wikiBreeds.find(r => r.name.toLowerCase() === val);
|
||||||
|
if (match) {
|
||||||
|
rasseIdInput.value = match.id;
|
||||||
|
if (matchBadge) matchBadge.style.display = '';
|
||||||
|
} else {
|
||||||
|
rasseIdInput.value = '';
|
||||||
|
if (matchBadge) matchBadge.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Foto-Vorschau
|
// Foto-Vorschau
|
||||||
const fotoInput = document.getElementById('dp-form-foto');
|
const fotoInput = document.getElementById('dp-form-foto');
|
||||||
const fotoPreview = document.getElementById('dp-form-preview');
|
const fotoPreview = document.getElementById('dp-form-preview');
|
||||||
|
|
@ -935,6 +1105,7 @@ window.Page_dog_profile = (() => {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: fd.name.trim(),
|
name: fd.name.trim(),
|
||||||
rasse: fd.rasse || null,
|
rasse: fd.rasse || null,
|
||||||
|
rasse_id: fd.rasse_id ? parseInt(fd.rasse_id) : null,
|
||||||
geburtstag: fd.geburtstag || null,
|
geburtstag: fd.geburtstag || null,
|
||||||
geschlecht: fd.geschlecht || null,
|
geschlecht: fd.geschlecht || null,
|
||||||
gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null,
|
gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null,
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,12 @@ window.Page_social = (() => {
|
||||||
🎾 Trainingstipp generieren
|
🎾 Trainingstipp generieren
|
||||||
<span style="font-size:10px;opacity:.7;margin-left:6px">104 Übungen</span>
|
<span style="font-size:10px;opacity:.7;margin-left:6px">104 Übungen</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="sm-pflege-tip" class="btn btn-secondary"
|
||||||
|
style="width:100%;min-height:44px;font-size:var(--text-sm);
|
||||||
|
margin-bottom:4px;border:1.5px solid #a78bfa;color:#a78bfa">
|
||||||
|
🛁 Pflegetipp generieren
|
||||||
|
<span style="font-size:10px;opacity:.7;margin-left:6px">allg. oder für gewählte Rasse</span>
|
||||||
|
</button>
|
||||||
<button id="sm-show-exercises" class="btn btn-secondary"
|
<button id="sm-show-exercises" class="btn btn-secondary"
|
||||||
style="width:100%;min-height:36px;font-size:11px;
|
style="width:100%;min-height:36px;font-size:11px;
|
||||||
margin-bottom:8px;color:var(--c-text-muted)">
|
margin-bottom:8px;color:var(--c-text-muted)">
|
||||||
|
|
@ -514,6 +520,43 @@ window.Page_social = (() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Pflegetipp
|
||||||
|
el.querySelector('#sm-pflege-tip').addEventListener('click', async () => {
|
||||||
|
const btn = el.querySelector('#sm-pflege-tip');
|
||||||
|
const res = el.querySelector('#sm-gen-result');
|
||||||
|
btn.disabled = true;
|
||||||
|
res.innerHTML = _lunaProgressHtml();
|
||||||
|
const interval = _startProgress(res);
|
||||||
|
const breedId = parseInt(el.querySelector('#sm-breed-id')?.value) || null;
|
||||||
|
try {
|
||||||
|
const url = breedId ? `/social/pflege-tipp?breed_id=${breedId}` : '/social/pflege-tipp';
|
||||||
|
const data = await API.post(url, {});
|
||||||
|
clearInterval(interval.bar); clearInterval(interval.msg);
|
||||||
|
_progressDone(res);
|
||||||
|
await new Promise(r => setTimeout(r, 400));
|
||||||
|
res.innerHTML = `
|
||||||
|
<div style="background:var(--c-surface-2);border-radius:10px;padding:10px;
|
||||||
|
margin-bottom:10px;display:flex;gap:10px;align-items:center">
|
||||||
|
<span style="font-size:2em;flex-shrink:0">🛁</span>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:11px;color:var(--c-text-muted)">
|
||||||
|
Pflegetipp · ${_esc(data.pflege_kat||'')}
|
||||||
|
${data.rasse_name ? ` · speziell für ${_esc(data.rasse_name)}` : ''}</div>
|
||||||
|
<div style="font-weight:700;font-size:var(--text-base)">
|
||||||
|
${_esc(data.pflege_titel||'')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${_renderResult(data, null)}`;
|
||||||
|
_bindResultEvents(res);
|
||||||
|
Promise.all([API.get('/social/stats'), API.get('/social/diversity')])
|
||||||
|
.then(([s,d]) => { _stats = s; _diversity = d; });
|
||||||
|
} catch(e) {
|
||||||
|
clearInterval(interval.bar); clearInterval(interval.msg);
|
||||||
|
res.innerHTML = `<div style="color:var(--c-danger);padding:var(--space-3)">
|
||||||
|
😬 ${_esc(e.message||String(e))}</div>`;
|
||||||
|
} finally { btn.disabled = false; }
|
||||||
|
});
|
||||||
|
|
||||||
// Rasse des Tages
|
// Rasse des Tages
|
||||||
el.querySelector('#sm-breed-day').addEventListener('click', async () => {
|
el.querySelector('#sm-breed-day').addEventListener('click', async () => {
|
||||||
const btn = el.querySelector('#sm-breed-day');
|
const btn = el.querySelector('#sm-breed-day');
|
||||||
|
|
@ -628,7 +671,7 @@ window.Page_social = (() => {
|
||||||
|
|
||||||
function _startProgress(container) {
|
function _startProgress(container) {
|
||||||
let elapsed = 0, msgIdx = 0;
|
let elapsed = 0, msgIdx = 0;
|
||||||
const totalMs = 14000;
|
const totalMs = 2800;
|
||||||
|
|
||||||
// Fortschrittsbalken: alle 250ms
|
// Fortschrittsbalken: alle 250ms
|
||||||
const barInterval = setInterval(() => {
|
const barInterval = setInterval(() => {
|
||||||
|
|
@ -694,14 +737,48 @@ window.Page_social = (() => {
|
||||||
</div>
|
</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap">
|
||||||
<span style="background:#f0fdf4;color:var(--c-success);border-radius:8px;
|
<span style="background:#f0fdf4;color:var(--c-success);border-radius:8px;
|
||||||
padding:4px 10px;font-size:11px;font-weight:600">✓ Gespeichert</span>
|
padding:4px 10px;font-size:11px;font-weight:600">✓ Gespeichert</span>
|
||||||
${score ? `<span>${score}</span>` : ''}
|
${score ? `<span>${score}</span>` : ''}
|
||||||
<button class="btn btn-sm btn-secondary sm-preview-btn"
|
<button class="btn btn-sm btn-secondary sm-preview-btn"
|
||||||
data-id="${data.id}"
|
data-id="${data.id}"
|
||||||
style="margin-left:auto;font-size:11px;padding:4px 10px;min-height:30px">
|
style="font-size:11px;padding:4px 10px;min-height:30px">
|
||||||
👁 Vorschau</button>
|
👁 Vorschau</button>
|
||||||
|
<button class="btn btn-sm btn-primary sm-posted-btn"
|
||||||
|
data-id="${data.id}"
|
||||||
|
style="margin-left:auto;font-size:11px;padding:4px 12px;min-height:30px;
|
||||||
|
background:#10b981;border-color:#10b981">
|
||||||
|
📤 Habe ich gepostet!
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="sm-posted-form-${data.id}" style="display:none;background:var(--c-surface-2);
|
||||||
|
border-radius:10px;padding:12px;margin-bottom:12px">
|
||||||
|
<div style="font-size:12px;font-weight:600;margin-bottom:8px">
|
||||||
|
🎉 Super! Kurze Angaben zum Post:</div>
|
||||||
|
<div style="display:grid;gap:8px">
|
||||||
|
<div>
|
||||||
|
<div class="sm-label">Datum (leer = heute)</div>
|
||||||
|
<input type="date" class="sm-post-date" data-id="${data.id}"
|
||||||
|
style="width:100%;background:var(--c-surface);color:var(--c-text);
|
||||||
|
border:1.5px solid var(--c-border);border-radius:8px;
|
||||||
|
padding:8px 12px;font-size:var(--text-sm);font-family:inherit;
|
||||||
|
box-sizing:border-box">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="sm-label">Post-URL (optional)</div>
|
||||||
|
<input type="url" class="sm-post-url" data-id="${data.id}"
|
||||||
|
placeholder="https://www.instagram.com/p/..."
|
||||||
|
style="width:100%;background:var(--c-surface);color:var(--c-text);
|
||||||
|
border:1.5px solid var(--c-border);border-radius:8px;
|
||||||
|
padding:8px 12px;font-size:var(--text-sm);font-family:inherit;
|
||||||
|
box-sizing:border-box">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary sm-confirm-posted" data-id="${data.id}"
|
||||||
|
style="min-height:40px">
|
||||||
|
✓ Bestätigen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${mediaUrl ? `
|
${mediaUrl ? `
|
||||||
|
|
@ -791,6 +868,47 @@ window.Page_social = (() => {
|
||||||
if (item) _showPreview(item);
|
if (item) _showPreview(item);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// "Habe ich gepostet!" — Formular einblenden
|
||||||
|
el.querySelectorAll('.sm-posted-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const form = el.querySelector(`#sm-posted-form-${btn.dataset.id}`);
|
||||||
|
if (form) {
|
||||||
|
form.style.display = form.style.display === 'none' ? '' : 'none';
|
||||||
|
// Heute als Default-Datum
|
||||||
|
const dateInput = form.querySelector('.sm-post-date');
|
||||||
|
if (dateInput && !dateInput.value) {
|
||||||
|
dateInput.value = new Date().toISOString().slice(0,10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Bestätigen
|
||||||
|
el.querySelectorAll('.sm-confirm-posted').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const id = parseInt(btn.dataset.id);
|
||||||
|
const form = el.querySelector(`#sm-posted-form-${id}`);
|
||||||
|
const date = form?.querySelector('.sm-post-date')?.value
|
||||||
|
|| new Date().toISOString().slice(0,16);
|
||||||
|
const url = form?.querySelector('.sm-post-url')?.value || null;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '…';
|
||||||
|
await API.patch(`/social/content/${id}`, {
|
||||||
|
status: 'published',
|
||||||
|
published_at: date,
|
||||||
|
post_url: url || undefined,
|
||||||
|
});
|
||||||
|
// Form durch Bestätigung ersetzen
|
||||||
|
if (form) form.innerHTML = `
|
||||||
|
<div style="text-align:center;padding:8px;color:var(--c-success);
|
||||||
|
font-weight:600;font-size:var(--text-sm)">
|
||||||
|
🎉 Super! Post als veröffentlicht markiert.
|
||||||
|
${url ? `<br><a href="${_esc(url)}" target="_blank" rel="noopener"
|
||||||
|
style="font-size:11px;color:var(--c-primary)">Post ansehen →</a>` : ''}
|
||||||
|
</div>`;
|
||||||
|
// Stats aktualisieren
|
||||||
|
API.get('/social/stats').then(s => { _stats = s; });
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
@ -883,12 +1001,28 @@ window.Page_social = (() => {
|
||||||
async function load(f) {
|
async function load(f) {
|
||||||
filter = f;
|
filter = f;
|
||||||
const url = f==='alle' ? '/social/content' : `/social/content?status=${f}`;
|
const url = f==='alle' ? '/social/content' : `/social/content?status=${f}`;
|
||||||
const items = await API.get(url).catch(() => []);
|
const [items, allItems] = await Promise.all([
|
||||||
render(items);
|
API.get(url).catch(() => []),
|
||||||
|
f !== 'alle' ? API.get('/social/content').catch(() => []) : Promise.resolve(null),
|
||||||
|
]);
|
||||||
|
const pending = (allItems || items).filter(c =>
|
||||||
|
c.status === 'idea' || c.status === 'draft').length;
|
||||||
|
render(items, pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(items) {
|
function render(items, pending) {
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
|
${pending > 0 ? `
|
||||||
|
<div style="background:var(--c-surface-2);border:1.5px solid var(--c-warning);
|
||||||
|
border-radius:10px;padding:10px 12px;margin-bottom:12px;
|
||||||
|
display:flex;align-items:center;gap:10px;font-size:var(--text-sm)">
|
||||||
|
<span style="font-size:1.3em;flex-shrink:0">⏳</span>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600;color:var(--c-warning)">${pending} Post${pending>1?'s':''} warten auf Bestätigung</div>
|
||||||
|
<div style="font-size:11px;color:var(--c-text-secondary)">
|
||||||
|
Tippe auf 📤 wenn du einen Post abgesetzt hast — so lernt Luna was wirklich live ging.</div>
|
||||||
|
</div>
|
||||||
|
</div>` : ''}
|
||||||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:var(--space-3)">
|
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:var(--space-3)">
|
||||||
${['alle','idea','draft','scheduled','published','archived'].map(s => `
|
${['alle','idea','draft','scheduled','published','archived'].map(s => `
|
||||||
<button class="btn btn-sm ${filter===s?'btn-primary':'btn-secondary'}"
|
<button class="btn btn-sm ${filter===s?'btn-primary':'btn-secondary'}"
|
||||||
|
|
@ -916,6 +1050,13 @@ window.Page_social = (() => {
|
||||||
font-style:italic">🎣 ${_esc(c.hook)}</div>` : ''}
|
font-style:italic">🎣 ${_esc(c.hook)}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
|
<div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
|
||||||
|
${c.status !== 'published' ? `
|
||||||
|
<button class="btn btn-sm sm-quick-post" data-id="${c.id}"
|
||||||
|
style="padding:3px 8px;font-size:11px;min-height:28px;
|
||||||
|
background:#10b981;border:1px solid #10b981;
|
||||||
|
color:#fff;border-radius:6px;cursor:pointer">
|
||||||
|
📤</button>` : `
|
||||||
|
<div style="text-align:center;font-size:18px;padding:3px 8px">✅</div>`}
|
||||||
<button class="btn btn-sm btn-secondary sm-exp"
|
<button class="btn btn-sm btn-secondary sm-exp"
|
||||||
data-id="${c.id}" style="padding:3px 8px;font-size:11px;min-height:28px">
|
data-id="${c.id}" style="padding:3px 8px;font-size:11px;min-height:28px">
|
||||||
Details</button>
|
Details</button>
|
||||||
|
|
@ -953,6 +1094,15 @@ window.Page_social = (() => {
|
||||||
</div>`).join('')}`;
|
</div>`).join('')}`;
|
||||||
|
|
||||||
el.querySelectorAll('[data-f]').forEach(b => b.addEventListener('click', () => load(b.dataset.f)));
|
el.querySelectorAll('[data-f]').forEach(b => b.addEventListener('click', () => load(b.dataset.f)));
|
||||||
|
el.querySelectorAll('.sm-quick-post').forEach(b => b.addEventListener('click', async () => {
|
||||||
|
const url = prompt('Post-URL (optional, leer lassen wenn keine):', '') ?? null;
|
||||||
|
await API.patch(`/social/content/${b.dataset.id}`, {
|
||||||
|
status: 'published',
|
||||||
|
published_at: new Date().toISOString().slice(0,16),
|
||||||
|
post_url: url || undefined,
|
||||||
|
});
|
||||||
|
load(filter);
|
||||||
|
}));
|
||||||
el.querySelectorAll('.sm-exp').forEach(b => b.addEventListener('click', () => {
|
el.querySelectorAll('.sm-exp').forEach(b => b.addEventListener('click', () => {
|
||||||
const d = el.querySelector(`#sm-d-${b.dataset.id}`);
|
const d = el.querySelector(`#sm-d-${b.dataset.id}`);
|
||||||
if (d) { d.style.display = d.style.display==='none'?'':'none'; }
|
if (d) { d.style.display = d.style.display==='none'?'':'none'; }
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v349';
|
const CACHE_VERSION = 'by-v356';
|
||||||
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