Social: Trainingstipp-Generator, Übungen in DB, 3 Stil-Varianten
- training_exercises Tabelle mit 13 Übungen aus App-Bibliothek - POST /social/training-tip: Stil-Varianten tutorial/community/aspirational - exercise_id in social_content für Wiederholungs-Tracking - Admin-Stats: Social-Media-Sektion mit Status-Übersicht + letzte 10 Posts - SW by-v349, APP_VER 336
This commit is contained in:
parent
ca0ce79815
commit
1cb0c2df77
8 changed files with 1497 additions and 337 deletions
|
|
@ -561,12 +561,35 @@ def _migrate(conn_factory):
|
||||||
published_at TEXT,
|
published_at TEXT,
|
||||||
source TEXT NOT NULL DEFAULT 'generated',
|
source TEXT NOT NULL DEFAULT 'generated',
|
||||||
breed_id INTEGER REFERENCES wiki_rassen(id) ON DELETE SET NULL,
|
breed_id INTEGER REFERENCES wiki_rassen(id) ON DELETE SET NULL,
|
||||||
notes TEXT
|
coaching TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
media_url TEXT,
|
||||||
|
category TEXT,
|
||||||
|
exercise_id TEXT
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_social_content_status
|
CREATE INDEX IF NOT EXISTS idx_social_content_status
|
||||||
ON social_content(status);
|
ON social_content(status);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Training-Übungen (für Social Media + Auswertung)
|
||||||
|
conn.executescript("""
|
||||||
|
CREATE TABLE IF NOT EXISTS training_exercises (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
exercise_id TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
kategorie TEXT NOT NULL,
|
||||||
|
schwierigkeit TEXT,
|
||||||
|
alter_ab TEXT,
|
||||||
|
dauer TEXT,
|
||||||
|
beschreibung TEXT,
|
||||||
|
schritte TEXT,
|
||||||
|
tipp TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_training_exercises_kat
|
||||||
|
ON training_exercises(kategorie);
|
||||||
|
""")
|
||||||
|
|
||||||
# Knigge: Community-Votes
|
# Knigge: Community-Votes
|
||||||
conn.executescript("""
|
conn.executescript("""
|
||||||
CREATE TABLE IF NOT EXISTS knigge_votes (
|
CREATE TABLE IF NOT EXISTS knigge_votes (
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,40 @@ async def stats(user=Depends(require_mod)):
|
||||||
except Exception:
|
except Exception:
|
||||||
ki_today = ki_month = ki_users_today = 0
|
ki_today = ki_month = ki_users_today = 0
|
||||||
|
|
||||||
|
# Social Media Tracking
|
||||||
|
try:
|
||||||
|
social_total = conn.execute("SELECT COUNT(*) FROM social_content").fetchone()[0]
|
||||||
|
social_published = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM social_content WHERE status='published'"
|
||||||
|
).fetchone()[0]
|
||||||
|
social_scheduled = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM social_content WHERE status='scheduled'"
|
||||||
|
).fetchone()[0]
|
||||||
|
social_ideas = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM social_content WHERE status='idea'"
|
||||||
|
).fetchone()[0]
|
||||||
|
social_this_week = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM social_content WHERE status='published' "
|
||||||
|
"AND published_at >= datetime('now', '-7 days')"
|
||||||
|
).fetchone()[0]
|
||||||
|
social_by_cat = {
|
||||||
|
row[0]: row[1] for row in conn.execute(
|
||||||
|
"SELECT category, COUNT(*) FROM social_content "
|
||||||
|
"WHERE category IS NOT NULL GROUP BY category ORDER BY 2 DESC"
|
||||||
|
).fetchall()
|
||||||
|
}
|
||||||
|
social_recent = [dict(r) for r in conn.execute(
|
||||||
|
"""SELECT topic, status, platform, format, created_at,
|
||||||
|
published_at, category, ai_score
|
||||||
|
FROM social_content
|
||||||
|
ORDER BY created_at DESC LIMIT 10"""
|
||||||
|
).fetchall()]
|
||||||
|
except Exception:
|
||||||
|
social_total = social_published = social_scheduled = 0
|
||||||
|
social_ideas = social_this_week = 0
|
||||||
|
social_by_cat = {}
|
||||||
|
social_recent = []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"users_total": users_total,
|
"users_total": users_total,
|
||||||
"users_today": users_today,
|
"users_today": users_today,
|
||||||
|
|
@ -183,6 +217,13 @@ 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,
|
||||||
|
"social_total": social_total,
|
||||||
|
"social_published": social_published,
|
||||||
|
"social_scheduled": social_scheduled,
|
||||||
|
"social_ideas": social_ideas,
|
||||||
|
"social_this_week": social_this_week,
|
||||||
|
"social_by_cat": social_by_cat,
|
||||||
|
"social_recent": social_recent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ KI-gestützte Content-Erstellung für TikTok & Instagram.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||||
|
|
@ -13,6 +14,101 @@ from pydantic import BaseModel
|
||||||
from auth import get_current_user, require_social_media
|
from auth import get_current_user, require_social_media
|
||||||
from database import db
|
from database import db
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Übungs-Bibliothek (gespiegelt aus uebungen.js)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
_UEBUNGEN = [
|
||||||
|
{"id": "grundkommandos_sitz", "name": "Sitz", "kat": "Grundkommando", "schwierigkeit": "Anfänger", "alter": "Ab 8 Wochen", "dauer": "3–5 Min",
|
||||||
|
"beschreibung": "Das erste Kommando — Basis für alles weitere. Leckerli über den Kopf führen, Hinterteil senkt sich, sofort belohnen.",
|
||||||
|
"schritte": ["Leckerli vor die Nase halten", "Langsam nach oben/hinten führen", "Hinterteil senkt sich → sofort Markerwort + Leckerli", "Erst nach 10x Wort 'Sitz' einführen"],
|
||||||
|
"tipp": "Nie zu früh das Kommandowort einführen — erst wenn die Bewegung sicher klappt."},
|
||||||
|
{"id": "grundkommandos_platz", "name": "Platz", "kat": "Grundkommando", "schwierigkeit": "Anfänger", "alter": "Ab 10 Wochen", "dauer": "3–5 Min",
|
||||||
|
"beschreibung": "Hund legt sich auf Signal hin. Leckerli senkrecht nach unten zwischen Vorderpfoten führen.",
|
||||||
|
"schritte": ["Hund ins Sitz", "Leckerli nach unten zwischen die Pfoten führen", "Ellenbogen + Hinterteil am Boden → Markerwort + Leckerli", "Wort 'Platz' nach 10–15 erfolgreichen Wiederholungen"],
|
||||||
|
"tipp": "Klappt nicht? Leckerli unter dein angewinkeltes Knie halten — Hund kriecht drunter durch."},
|
||||||
|
{"id": "grundkommandos_bleib", "name": "Bleib", "kat": "Grundkommando", "schwierigkeit": "Anfänger–Fortgeschr.", "alter": "Ab 12 Wochen", "dauer": "5 Min",
|
||||||
|
"beschreibung": "Drei Dimensionen: Dauer, Distanz, Ablenkung — immer nur eine steigern!",
|
||||||
|
"schritte": ["Hund ins Sitz", "2 Sekunden warten → Markerwort + Leckerli", "Freigabewort einführen: 'Okay'", "Dauer schrittweise: 2 → 5 → 10 → 30 Sekunden", "Erst dann einen Schritt zurücktreten"],
|
||||||
|
"tipp": "Immer zum Hund zurückkehren und belohnen — nie den Hund zu sich kommen lassen beim 'Bleib'."},
|
||||||
|
{"id": "grundkommandos_hier", "name": "Hier / Komm", "kat": "Grundkommando", "schwierigkeit": "Anfänger", "alter": "Ab 8 Wochen", "dauer": "5 Min",
|
||||||
|
"beschreibung": "Lebensrettend wichtig. Niemals rufen und dann etwas Unangenehmes tun.",
|
||||||
|
"schritte": ["In der Wohnung auf 2–3 Meter beginnen", "Hinknien, Arme öffnen, freudige Stimme: 'Hier!'", "Ankommen = große Freude + Leckerli", "Nur einmal rufen — wer mehrfach ruft, trainiert Ignorieren"],
|
||||||
|
"tipp": "Schleppleine im Garten: Hund kann nicht wegbleiben, Erfolg ist garantiert."},
|
||||||
|
{"id": "grundkommandos_fuss", "name": "Fuß", "kat": "Grundkommando", "schwierigkeit": "Fortgeschr. Anfänger", "alter": "Ab 12 Wochen", "dauer": "5–10 Min",
|
||||||
|
"beschreibung": "Hund läuft ruhig neben dir ohne zu ziehen — die Leine hängt locker durch.",
|
||||||
|
"schritte": ["Hund links positionieren, Leckerli auf Hüfthöhe halten", "Schritt vorwärts → Hund folgt", "Leine locker = belohnen", "Zieht er: stehen bleiben oder Richtung wechseln"],
|
||||||
|
"tipp": "Nie mitziehen lassen. Leine locker = Belohnung, Leine straff = Stopp."},
|
||||||
|
{"id": "grundkommandos_aus", "name": "Aus / Lass es", "kat": "Grundkommando", "schwierigkeit": "Anfänger", "alter": "Ab 10 Wochen", "dauer": "3–5 Min",
|
||||||
|
"beschreibung": "Hund lässt Gegenstände auf Kommando los — wichtig für Sicherheit.",
|
||||||
|
"schritte": ["Hund hält Spielzeug", "Leckerli vor Nase → er lässt los", "Markerwort + Leckerli", "Gegenstand zurückgeben → Loslassen lohnt sich!"],
|
||||||
|
"tipp": "Nie einfach wegnehmen nach 'Aus' — sonst gibt er nächstes Mal nicht mehr her."},
|
||||||
|
{"id": "tricks_pfote", "name": "Pfote / Handschlag", "kat": "Trick", "schwierigkeit": "Anfänger", "alter": "Ab 12 Wochen", "dauer": "3 Min",
|
||||||
|
"beschreibung": "Klassischer Trick — sieht toll aus und ist leicht zu lernen.",
|
||||||
|
"schritte": ["Leckerli in der Faust verstecken, Faust auf Kniehöhe", "Hund schnuppert und kratzt irgendwann → sofort öffnen", "Auf flache Hand umstellen", "Wort 'Pfote' einführen"],
|
||||||
|
"tipp": "Auf flacher Hand wird aus dem Kratzen ein elegantes Ablegen der Pfote."},
|
||||||
|
{"id": "tricks_dreh", "name": "Dreh / Kreis", "kat": "Trick", "schwierigkeit": "Anfänger", "alter": "Ab 12 Wochen", "dauer": "3–5 Min",
|
||||||
|
"beschreibung": "Hund dreht eine Runde auf Handzeichen — sieht aus wie Tanzen!",
|
||||||
|
"schritte": ["Leckerli vor Nase", "Kreis in der Luft führen", "Volle Drehung → Markerwort + Leckerli", "'Dreh' = links, 'Runde' = rechts"],
|
||||||
|
"tipp": "Handbewegung schrittweise kleiner machen bis nur noch ein Fingerzeig reicht."},
|
||||||
|
{"id": "tricks_such", "name": "Suchspiel / Nasenarbeit", "kat": "Trick", "schwierigkeit": "Anfänger", "alter": "Ab 8 Wochen", "dauer": "5–10 Min",
|
||||||
|
"beschreibung": "Mentale Auslastung pur — 10 Min Suchen = 1 Stunde Spaziergang!",
|
||||||
|
"schritte": ["Leckerli unter Becher verstecken", "Hund findet es → belohnen", "Mehrere Becher, einer hat Leckerli", "Später im Gras oder draußen"],
|
||||||
|
"tipp": "Perfekt für Regentage oder wenn körperliche Aktivität nicht möglich ist."},
|
||||||
|
{"id": "tricks_decke", "name": "Platz auf Decke", "kat": "Trick", "schwierigkeit": "Fortgeschr. Anfänger", "alter": "Ab 4 Monate", "dauer": "5–10 Min",
|
||||||
|
"beschreibung": "Hund geht auf sein Platz und liegt ruhig — perfekt für Restaurant oder Besuche.",
|
||||||
|
"schritte": ["Decke hinlegen, Leckerli drauf werfen", "Jedes Betreten belohnen", "Auf das Hinlegen warten → große Belohnung", "Decke schrittweise weiter wegstellen"],
|
||||||
|
"tipp": "Ideal für den Alltag: Hund hat seinen sicheren Platz überall."},
|
||||||
|
{"id": "problem_springen", "name": "Nicht springen", "kat": "Problemverhalten","schwierigkeit": "Anfänger", "alter": "Ab 8 Wochen", "dauer": "Bei jeder Begrüßung",
|
||||||
|
"beschreibung": "Hund begrüßt mit allen vier Pfoten am Boden.",
|
||||||
|
"schritte": ["Springt er: keine Reaktion", "Alle vier Pfoten unten → sofort Markerwort + Leckerli", "Alle im Haushalt müssen gleich reagieren"],
|
||||||
|
"tipp": "Konsequenz ist alles — ein Mal hochspringen lassen reicht, um es wieder zu lernen."},
|
||||||
|
{"id": "problem_alleine", "name": "Alleine bleiben", "kat": "Problemverhalten","schwierigkeit": "Mittel", "alter": "Ab 10 Wochen", "dauer": "Mehrmals täglich",
|
||||||
|
"beschreibung": "Hund bleibt ruhig allein — ohne Stress, Bellen oder Zerstören.",
|
||||||
|
"schritte": ["Kong mit Futter füllen", "10 Sekunden rausgehen → ruhig zurückkommen", "Zeit schrittweise erhöhen", "Nie dramatisch verabschieden oder begrüßen"],
|
||||||
|
"tipp": "Welpen max. 1–2 Stunden, erwachsene Hunde max. 4–6 Stunden allein lassen."},
|
||||||
|
{"id": "problem_bellen", "name": "Weniger Bellen", "kat": "Problemverhalten","schwierigkeit": "Mittel", "alter": "Ab 8 Wochen", "dauer": "5–10 Min täglich",
|
||||||
|
"beschreibung": "Hund beruhigt sich auf Signal hin — kein übermäßiges Kläffen mehr.",
|
||||||
|
"schritte": ["Ursache kennen: Alarm, Frust, Aufmerksamkeit oder Angst?", "Aufmerksamkeits-Bellen nie belohnen", "Kurze Pause abwarten → sofort markieren + belohnen", "'Ruhig' Kommando einführen"],
|
||||||
|
"tipp": "Nie schreien oder mitbellen — der Hund denkt du machst mit!"},
|
||||||
|
]
|
||||||
|
|
||||||
|
_TRAINING_STILE = [
|
||||||
|
"tutorial", # "So geht das:"
|
||||||
|
"community", # "Sind grad dran das zu lernen"
|
||||||
|
"aspirational",# "Das könnte euer Hund auch"
|
||||||
|
]
|
||||||
|
|
||||||
|
_PROMPT_TRAINING = '''\
|
||||||
|
Erstelle einen Social-Media-Post über den Hundetraining-Tipp "{name}" für Ban Yaro (banyaro.app).
|
||||||
|
|
||||||
|
Kategorie: {kat}
|
||||||
|
Schwierigkeit: {schwierigkeit}
|
||||||
|
Alter: {alter}
|
||||||
|
Dauer: {dauer}
|
||||||
|
Beschreibung: {beschreibung}
|
||||||
|
Schritte: {schritte}
|
||||||
|
Profi-Tipp: {tipp}
|
||||||
|
|
||||||
|
Stil dieses Posts: "{stil}"
|
||||||
|
- "tutorial" → "Schaut, so geht das: [Schritte knapp erklärt]"
|
||||||
|
- "community" → "Sind grad dran das zu lernen 🙋 Wer kennt das?"
|
||||||
|
- "aspirational" → "Das könnte euer Hund auch — in nur [Dauer]!"
|
||||||
|
|
||||||
|
Antworte NUR als JSON:
|
||||||
|
{{
|
||||||
|
"caption": "Post-Text passend zum Stil, mit Emojis, max 600 Zeichen, lehrreich aber locker",
|
||||||
|
"hashtags": "10 Hashtags kommagetrennt ohne #: hundetraining, hundeschule, positivreinforcement, {name_lower}, banyaro + passende",
|
||||||
|
"hook": "Erste Zeile die sofort Aufmerksamkeit fängt",
|
||||||
|
"cta": "Frage ans Publikum: z.B. 'Übt ihr das auch gerade?' oder 'Zeigt uns eure Videos!'",
|
||||||
|
"visual_brief": "Was im Video/Foto zu sehen sein sollte: Person mit Hund beim Training, Leckerli sichtbar, bestimmte Körperhaltung",
|
||||||
|
"canva_notes": "Text-Overlay: Übungsname + 1 Key-Tipp als Grafik",
|
||||||
|
"unsplash_query": "dog training {name_en} positive reinforcement",
|
||||||
|
"ai_score": <1-5>,
|
||||||
|
"category": "training",
|
||||||
|
"coaching": "Tipp für die Creatorin: Warum spricht dieser Post die Zielgruppe an? Wie filmt man das am besten? (1-2 Sätze)"
|
||||||
|
}}
|
||||||
|
'''
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -56,9 +152,12 @@ Plattform: {platform}
|
||||||
Format: {format}
|
Format: {format}
|
||||||
Thema: {topic}
|
Thema: {topic}
|
||||||
{breed_info}
|
{breed_info}
|
||||||
Bereits verwendete Themen (nicht wiederholen):
|
Bereits verwendete Themen — NICHT wiederholen, ähnliche Inhalte VERMEIDEN:
|
||||||
{used_topics}
|
{used_topics}
|
||||||
|
|
||||||
|
Kategorie-Verteilung der letzten Posts (vermeide überfüllte Kategorien):
|
||||||
|
{category_stats}
|
||||||
|
|
||||||
Antworte NUR mit einem JSON-Objekt:
|
Antworte NUR mit einem JSON-Objekt:
|
||||||
{{
|
{{
|
||||||
"caption": "Post-Text (plattformgerecht, mit Emojis, max 2200 Zeichen für Instagram / 150 für TikTok)",
|
"caption": "Post-Text (plattformgerecht, mit Emojis, max 2200 Zeichen für Instagram / 150 für TikTok)",
|
||||||
|
|
@ -71,6 +170,7 @@ Antworte NUR mit einem JSON-Objekt:
|
||||||
"script": {script_field},
|
"script": {script_field},
|
||||||
"unsplash_query": "2-3 englische Suchbegriffe für Unsplash-Stockfotos",
|
"unsplash_query": "2-3 englische Suchbegriffe für Unsplash-Stockfotos",
|
||||||
"ai_score": <Zahl 1-5, geschätztes Engagement-Potenzial>,
|
"ai_score": <Zahl 1-5, geschätztes Engagement-Potenzial>,
|
||||||
|
"category": "Eine Kategorie aus: tipps|humor|rasse|community|gesundheit|training|saisonal|emotional|behind-the-scenes|quiz",
|
||||||
"coaching": "2-3 Sätze für die Creatorin: Warum dieser Hook? Warum diese Hashtags? Was macht diesen Post stark? Locker und ermutigend formuliert."
|
"coaching": "2-3 Sätze für die Creatorin: Warum dieser Hook? Warum diese Hashtags? Was macht diesen Post stark? Locker und ermutigend formuliert."
|
||||||
}}
|
}}
|
||||||
'''
|
'''
|
||||||
|
|
@ -134,6 +234,57 @@ def _used_topics(limit: int = 30) -> str:
|
||||||
return "\n".join(f"- {r['topic']}" for r in rows)
|
return "\n".join(f"- {r['topic']}" for r in rows)
|
||||||
|
|
||||||
|
|
||||||
|
def _category_stats(limit: int = 30) -> str:
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT category, COUNT(*) as n FROM social_content
|
||||||
|
WHERE category IS NOT NULL
|
||||||
|
ORDER BY created_at DESC LIMIT ?""",
|
||||||
|
(limit,),
|
||||||
|
).fetchall()
|
||||||
|
if not rows:
|
||||||
|
return "(noch keine Kategorien)"
|
||||||
|
from collections import Counter
|
||||||
|
counts = Counter(r["category"] for r in rows)
|
||||||
|
total = sum(counts.values())
|
||||||
|
lines = [f"- {cat}: {n}x ({round(n/total*100)}%)" for cat, n in counts.most_common()]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _diversity_warning(limit: int = 20) -> dict | None:
|
||||||
|
"""Gibt Warnung zurück wenn eine Kategorie >40% der letzten Posts dominiert."""
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT category FROM social_content
|
||||||
|
WHERE category IS NOT NULL
|
||||||
|
ORDER BY created_at DESC LIMIT ?""",
|
||||||
|
(limit,),
|
||||||
|
).fetchall()
|
||||||
|
if len(rows) < 5:
|
||||||
|
return None
|
||||||
|
from collections import Counter
|
||||||
|
counts = Counter(r["category"] for r in rows)
|
||||||
|
total = len(rows)
|
||||||
|
for cat, n in counts.most_common(1):
|
||||||
|
if n / total > 0.4:
|
||||||
|
_CAT_DE = {
|
||||||
|
"tipps": "Tipps", "humor": "Humor/Spaß", "rasse": "Rassen-Info",
|
||||||
|
"community": "Community", "gesundheit": "Gesundheit",
|
||||||
|
"training": "Training", "saisonal": "Saison-Content",
|
||||||
|
"emotional": "Emotionale Posts", "behind-the-scenes": "Behind the Scenes",
|
||||||
|
"quiz": "Quiz/Fragen",
|
||||||
|
}
|
||||||
|
missing = [c for c in _CAT_DE if c not in counts]
|
||||||
|
return {
|
||||||
|
"dominant": cat,
|
||||||
|
"dominant_de": _CAT_DE.get(cat, cat),
|
||||||
|
"pct": round(n / total * 100),
|
||||||
|
"suggestions": missing[:3],
|
||||||
|
"suggestions_de": [_CAT_DE.get(c, c) for c in missing[:3]],
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _breed_info(breed_id: int | None) -> str:
|
def _breed_info(breed_id: int | None) -> str:
|
||||||
if not breed_id:
|
if not breed_id:
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -183,6 +334,24 @@ def _parse_json(raw: str) -> dict:
|
||||||
raise ValueError(f"Kein JSON in Antwort: {raw[:200]}")
|
raise ValueError(f"Kein JSON in Antwort: {raw[:200]}")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/social/diversity — Kreativitäts-Check
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/diversity")
|
||||||
|
async def get_diversity(user=Depends(require_social_media)):
|
||||||
|
warning = _diversity_warning()
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT category, COUNT(*) as n FROM social_content
|
||||||
|
WHERE category IS NOT NULL
|
||||||
|
ORDER BY n DESC""",
|
||||||
|
).fetchall()
|
||||||
|
return {
|
||||||
|
"warning": warning,
|
||||||
|
"distribution": [dict(r) for r in rows],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# GET /api/social/suggestions — KI schlägt 5 Themen vor
|
# GET /api/social/suggestions — KI schlägt 5 Themen vor
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
@ -254,6 +423,7 @@ async def generate_content(req: GenerateRequest, user=Depends(require_social_med
|
||||||
topic=req.topic,
|
topic=req.topic,
|
||||||
breed_info=_breed_info(req.breed_id),
|
breed_info=_breed_info(req.breed_id),
|
||||||
used_topics=_used_topics(),
|
used_topics=_used_topics(),
|
||||||
|
category_stats=_category_stats(),
|
||||||
script_field=script_field,
|
script_field=script_field,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -269,8 +439,8 @@ async def generate_content(req: GenerateRequest, user=Depends(require_social_med
|
||||||
"""INSERT INTO social_content
|
"""INSERT INTO social_content
|
||||||
(created_by, platform, format, topic, caption, hashtags,
|
(created_by, platform, format, topic, caption, hashtags,
|
||||||
visual_brief, image_prompt, canva_notes, script, hook, cta,
|
visual_brief, image_prompt, canva_notes, script, hook, cta,
|
||||||
unsplash_query, ai_score, source, breed_id, coaching)
|
unsplash_query, ai_score, source, breed_id, coaching, category)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
(
|
(
|
||||||
user["id"], req.platform, req.format, req.topic,
|
user["id"], req.platform, req.format, req.topic,
|
||||||
data.get("caption"), data.get("hashtags"),
|
data.get("caption"), data.get("hashtags"),
|
||||||
|
|
@ -278,7 +448,7 @@ async def generate_content(req: GenerateRequest, user=Depends(require_social_med
|
||||||
data.get("canva_notes"), data.get("script"),
|
data.get("canva_notes"), data.get("script"),
|
||||||
data.get("hook"), data.get("cta"),
|
data.get("hook"), data.get("cta"),
|
||||||
data.get("unsplash_query"), data.get("ai_score"),
|
data.get("unsplash_query"), data.get("ai_score"),
|
||||||
"generated", req.breed_id, data.get("coaching"),
|
"generated", req.breed_id, data.get("coaching"), data.get("category"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
entry_id = cur.lastrowid
|
entry_id = cur.lastrowid
|
||||||
|
|
@ -369,7 +539,121 @@ async def delete_content(cid: int, user=Depends(require_social_media)):
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# GET /api/social/breeds — Rassen für Dropdown
|
# Übungen in DB seeden (einmalig beim Import)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _seed_exercises():
|
||||||
|
with db() as conn:
|
||||||
|
for u in _UEBUNGEN:
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT OR IGNORE INTO training_exercises
|
||||||
|
(exercise_id, name, kategorie, schwierigkeit, alter_ab,
|
||||||
|
dauer, beschreibung, schritte, tipp)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||||||
|
(u["id"], u["name"], u["kat"], u["schwierigkeit"],
|
||||||
|
u.get("alter"), u.get("dauer"), u.get("beschreibung"),
|
||||||
|
json.dumps(u.get("schritte", []), ensure_ascii=False),
|
||||||
|
u.get("tipp")),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_seed_exercises()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /api/social/training-tip — Trainingstipp-Post generieren
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.post("/training-tip")
|
||||||
|
async def training_tip(user=Depends(require_social_media)):
|
||||||
|
# Übung wählen die noch nicht als Social-Post verwendet wurde
|
||||||
|
with db() as conn:
|
||||||
|
used = {r["exercise_id"] for r in conn.execute(
|
||||||
|
"SELECT exercise_id FROM social_content WHERE exercise_id IS NOT NULL"
|
||||||
|
).fetchall()}
|
||||||
|
all_ex = conn.execute(
|
||||||
|
"SELECT * FROM training_exercises ORDER BY RANDOM()"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
unused = [e for e in all_ex if e["exercise_id"] not in used]
|
||||||
|
pool = unused if unused else list(all_ex) # Reset wenn alle durch
|
||||||
|
if not pool:
|
||||||
|
raise HTTPException(404, "Keine Übungen gefunden.")
|
||||||
|
|
||||||
|
ex = dict(pool[0])
|
||||||
|
stil = random.choice(_TRAINING_STILE)
|
||||||
|
schritte_list = json.loads(ex["schritte"] or "[]")
|
||||||
|
schritte_text = "\n".join(f" {i+1}. {s}" for i, s in enumerate(schritte_list[:4]))
|
||||||
|
|
||||||
|
prompt = _PROMPT_TRAINING.format(
|
||||||
|
name=ex["name"],
|
||||||
|
kat=ex["kategorie"],
|
||||||
|
schwierigkeit=ex["schwierigkeit"] or "Anfänger",
|
||||||
|
alter=ex["alter_ab"] or "Ab 8 Wochen",
|
||||||
|
dauer=ex["dauer"] or "5 Min",
|
||||||
|
beschreibung=ex["beschreibung"] or ex["name"],
|
||||||
|
schritte=schritte_text,
|
||||||
|
tipp=ex["tipp"] or "",
|
||||||
|
stil=stil,
|
||||||
|
name_lower=ex["name"].lower().replace(" ", ""),
|
||||||
|
name_en=ex["name"],
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = await _ki_complete(prompt)
|
||||||
|
data = _parse_json(raw)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, f"KI-Fehler: {e}")
|
||||||
|
|
||||||
|
stil_label = {
|
||||||
|
"tutorial": f"So geht's: {ex['name']}",
|
||||||
|
"community": f"Lernen gerade: {ex['name']} 🙋",
|
||||||
|
"aspirational": f"Das kann dein Hund auch: {ex['name']}",
|
||||||
|
}[stil]
|
||||||
|
|
||||||
|
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", "reel" if stil == "tutorial" else "post",
|
||||||
|
stil_label, 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"),
|
||||||
|
"training", ex["exercise_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["exercise_name"] = ex["name"]
|
||||||
|
result["exercise_kat"] = ex["kategorie"]
|
||||||
|
result["stil"] = stil
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/social/exercises — alle Übungen mit Nutzungsstatistik
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/exercises")
|
||||||
|
async def get_exercises(user=Depends(require_social_media)):
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT e.*,
|
||||||
|
(SELECT COUNT(*) FROM social_content s
|
||||||
|
WHERE s.exercise_id = e.exercise_id) as posts_count
|
||||||
|
FROM training_exercises e
|
||||||
|
ORDER BY e.kategorie, e.name"""
|
||||||
|
).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/social/breeds — alle Rassen für Autocomplete
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@router.get("/breeds")
|
@router.get("/breeds")
|
||||||
async def social_breeds(user=Depends(require_social_media)):
|
async def social_breeds(user=Depends(require_social_media)):
|
||||||
|
|
@ -380,6 +664,131 @@ async def social_breeds(user=Depends(require_social_media)):
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/social/unused-breeds — noch nie in Post verwendete Rassen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/unused-breeds")
|
||||||
|
async def unused_breeds(limit: int = 6, user=Depends(require_social_media)):
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT r.id, r.name FROM wiki_rassen r
|
||||||
|
WHERE r.ki_enriched = 1
|
||||||
|
AND r.id NOT IN (
|
||||||
|
SELECT breed_id FROM social_content
|
||||||
|
WHERE breed_id IS NOT NULL
|
||||||
|
)
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT ?""",
|
||||||
|
(limit,),
|
||||||
|
).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /api/social/breed-of-day — "Wusstest du schon?" Post generieren
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
_PROMPT_BREED_DAY = '''\
|
||||||
|
Erstelle einen begeisternden Social-Media-Post über die Hunderasse "{name}" für Ban Yaro (banyaro.app).
|
||||||
|
|
||||||
|
Rassen-Daten aus unserer Datenbank:
|
||||||
|
- Herkunft: {herkunft}
|
||||||
|
- Größe: {groesse}
|
||||||
|
- Gewicht: {gewicht_min}–{gewicht_max} kg
|
||||||
|
- Lebensdauer: {lebensdauer}
|
||||||
|
- Aktivität: {aktivitaet}
|
||||||
|
- Erfahrung: {erfahrung}
|
||||||
|
- Temperament: {temperament}
|
||||||
|
- Für Kinder geeignet: {kinder}
|
||||||
|
- Wohnungsgeeignet: {wohnung}
|
||||||
|
- Beschreibung: {beschreibung}
|
||||||
|
|
||||||
|
Antworte NUR als JSON:
|
||||||
|
{{
|
||||||
|
"caption": "Post-Text der mit 🐾 Wusstest du schon, wie cool der/die {name} ist? beginnt. Fakten aus den Daten, mit Emojis, max 800 Zeichen.",
|
||||||
|
"hashtags": "10-12 Hashtags kommagetrennt ohne #: Rassenname, hundeleben, banyaro, passende Eigenschaften",
|
||||||
|
"hook": "Erste Zeile als Aufmerksamkeitsfänger",
|
||||||
|
"cta": "Frage ans Publikum passend zur Rasse",
|
||||||
|
"visual_brief": "Beschreibung: typisches Bild dieser Rasse, Stimmung, Setting",
|
||||||
|
"canva_notes": "Textüberlagerungen für das Rassenfoto: Rassenname groß, 1-2 Fakten",
|
||||||
|
"unsplash_query": "2-3 englische Suchbegriffe für diese Rasse",
|
||||||
|
"ai_score": <1-5>,
|
||||||
|
"category": "rasse",
|
||||||
|
"coaching": "Tipp für die Creatorin: Warum ist diese Rasse spannend für die Zielgruppe? (1-2 Sätze)"
|
||||||
|
}}
|
||||||
|
'''
|
||||||
|
|
||||||
|
@router.post("/breed-of-day")
|
||||||
|
async def breed_of_day(user=Depends(require_social_media)):
|
||||||
|
with db() as conn:
|
||||||
|
rasse = conn.execute(
|
||||||
|
"""SELECT r.id, r.name, r.herkunft, r.groesse,
|
||||||
|
r.gewicht_min_kg, r.gewicht_max_kg, r.lebensdauer,
|
||||||
|
r.aktivitaet, r.erfahrung, r.temperament,
|
||||||
|
r.kinder_geeignet, r.wohnung_geeignet,
|
||||||
|
r.beschreibung, r.foto_url
|
||||||
|
FROM wiki_rassen r
|
||||||
|
WHERE r.ki_enriched = 1
|
||||||
|
AND r.beschreibung IS NOT NULL
|
||||||
|
AND r.id NOT IN (
|
||||||
|
SELECT breed_id FROM social_content WHERE breed_id IS NOT NULL
|
||||||
|
)
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT 1""",
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if not rasse:
|
||||||
|
raise HTTPException(404, "Alle Rassen wurden bereits verwendet — Reset nötig.")
|
||||||
|
|
||||||
|
def _yn(v): return "Ja" if v else "Nein"
|
||||||
|
prompt = _PROMPT_BREED_DAY.format(
|
||||||
|
name=rasse["name"],
|
||||||
|
herkunft=rasse["herkunft"] or "unbekannt",
|
||||||
|
groesse=rasse["groesse"] or "unbekannt",
|
||||||
|
gewicht_min=rasse["gewicht_min_kg"] or "?",
|
||||||
|
gewicht_max=rasse["gewicht_max_kg"] or "?",
|
||||||
|
lebensdauer=rasse["lebensdauer"] or "unbekannt",
|
||||||
|
aktivitaet=rasse["aktivitaet"] or "mittel",
|
||||||
|
erfahrung=rasse["erfahrung"] or "fortgeschritten",
|
||||||
|
temperament=rasse["temperament"] or "unbekannt",
|
||||||
|
kinder=_yn(rasse["kinder_geeignet"]),
|
||||||
|
wohnung=_yn(rasse["wohnung_geeignet"]),
|
||||||
|
beschreibung=(rasse["beschreibung"] or "")[:400],
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = await _ki_complete(prompt)
|
||||||
|
data = _parse_json(raw)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, f"KI-Fehler: {e}")
|
||||||
|
|
||||||
|
with db() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
"""INSERT INTO social_content
|
||||||
|
(created_by, platform, format, topic, caption, hashtags,
|
||||||
|
visual_brief, image_prompt, canva_notes, hook, cta,
|
||||||
|
unsplash_query, ai_score, source, breed_id, coaching,
|
||||||
|
category, media_url)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
|
(
|
||||||
|
user["id"], "both", "post",
|
||||||
|
f"Rasse des Tages: {rasse['name']}",
|
||||||
|
data.get("caption"), data.get("hashtags"),
|
||||||
|
data.get("visual_brief"), None,
|
||||||
|
data.get("canva_notes"), data.get("hook"), data.get("cta"),
|
||||||
|
data.get("unsplash_query"), data.get("ai_score"),
|
||||||
|
"generated", rasse["id"], data.get("coaching"),
|
||||||
|
"rasse", rasse["foto_url"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
entry_id = cur.lastrowid
|
||||||
|
|
||||||
|
with db() as conn:
|
||||||
|
row = conn.execute("SELECT * FROM social_content WHERE id=?", (entry_id,)).fetchone()
|
||||||
|
result = dict(row)
|
||||||
|
result["breed_foto"] = rasse["foto_url"]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# GET /api/social/stats — Level / XP
|
# GET /api/social/stats — Level / XP
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@
|
||||||
|
|
||||||
<div class="sidebar-item" data-page="social" id="sidebar-social"
|
<div class="sidebar-item" data-page="social" id="sidebar-social"
|
||||||
style="display:none;color:var(--c-warning,#f59e0b)">
|
style="display:none;color:var(--c-warning,#f59e0b)">
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#instagram-logo"></use></svg> Social Media
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg> Social Media
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-item" data-page="admin" id="sidebar-admin"
|
<div class="sidebar-item" data-page="admin" id="sidebar-admin"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '325'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '336'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
|
|
||||||
const App = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,59 @@ window.Page_admin = (() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Social Media Tracking -->
|
||||||
|
<div class="card" style="padding:var(--space-4)">
|
||||||
|
<p style="font-size:var(--text-sm);font-weight:600;margin:0 0 var(--space-3)">
|
||||||
|
📱 Social Media Tracking</p>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2);
|
||||||
|
margin-bottom:var(--space-3)">
|
||||||
|
${[
|
||||||
|
['Gesamt generiert', s.social_total, 'var(--c-text-secondary)'],
|
||||||
|
['Veröffentlicht', s.social_published, 'var(--c-success)'],
|
||||||
|
['Geplant', s.social_scheduled, 'var(--c-primary)'],
|
||||||
|
['Ideen offen', s.social_ideas, 'var(--c-warning)'],
|
||||||
|
['Diese Woche live', s.social_this_week, 'var(--c-success)'],
|
||||||
|
].map(([label, val, color]) => `
|
||||||
|
<div style="background:var(--c-surface-2);border-radius:8px;padding:10px">
|
||||||
|
<div style="font-size:10px;color:var(--c-text-muted);margin-bottom:2px">${label}</div>
|
||||||
|
<div style="font-size:1.4em;font-weight:700;color:${color}">${val ?? 0}</div>
|
||||||
|
</div>`).join('')}
|
||||||
|
</div>
|
||||||
|
${Object.keys(s.social_by_cat||{}).length ? `
|
||||||
|
<div style="margin-bottom:var(--space-3)">
|
||||||
|
<div style="font-size:11px;color:var(--c-text-muted);margin-bottom:6px;
|
||||||
|
font-weight:600;text-transform:uppercase">Kategorien</div>
|
||||||
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||||
|
${Object.entries(s.social_by_cat).map(([cat, n]) => `
|
||||||
|
<span style="background:var(--c-surface-2);border-radius:20px;
|
||||||
|
padding:2px 10px;font-size:11px">
|
||||||
|
${cat} <strong>${n}</strong></span>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>` : ''}
|
||||||
|
${s.social_recent?.length ? `
|
||||||
|
<div>
|
||||||
|
<div style="font-size:11px;color:var(--c-text-muted);margin-bottom:6px;
|
||||||
|
font-weight:600;text-transform:uppercase">Letzte 10 Posts</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:6px">
|
||||||
|
${s.social_recent.map(p => `
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;
|
||||||
|
font-size:var(--text-xs);padding:6px 0;
|
||||||
|
border-bottom:1px solid var(--c-border)">
|
||||||
|
<span style="padding:2px 7px;border-radius:4px;font-size:10px;
|
||||||
|
background:var(--c-surface-2);flex-shrink:0;
|
||||||
|
color:${{idea:'var(--c-text-muted)',draft:'var(--c-warning)',
|
||||||
|
scheduled:'var(--c-primary)',published:'var(--c-success)',
|
||||||
|
archived:'var(--c-text-muted)'}[p.status]||'inherit'}">
|
||||||
|
${p.status}</span>
|
||||||
|
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;
|
||||||
|
white-space:nowrap">${p.topic}</span>
|
||||||
|
<span style="color:var(--c-text-muted);flex-shrink:0">
|
||||||
|
${(p.published_at||p.created_at||'').slice(0,10)}</span>
|
||||||
|
</div>`).join('')}
|
||||||
|
</div>
|
||||||
|
</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);color:var(--c-text-secondary);margin:0;line-height:1.6">
|
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0;line-height:1.6">
|
||||||
<svg class="ph-icon" aria-hidden="true" style="color:var(--c-primary)">
|
<svg class="ph-icon" aria-hidden="true" style="color:var(--c-primary)">
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v338';
|
const CACHE_VERSION = 'by-v349';
|
||||||
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