From 9ebd40aaae9fed0559bcc1ffe8b78c778fa540e2 Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 8 May 2026 18:24:27 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20Geburtstags-KI=20=E2=80=94=202=20Mod?= =?UTF-8?q?i=20(morgen:=20Ideen,=20heute:=20Hundeperspektive),=20DB-Cache,?= =?UTF-8?q?=20Abend-Tagebuch-Hinweis=20(SW=20by-v780)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/database.py | 12 +++++ backend/main.py | 2 +- backend/routes/ki.py | 79 ++++++++++++++++++++++++++++++++ backend/static/index.html | 8 ++-- backend/static/js/app.js | 2 +- backend/static/js/worlds.js | 90 +++++++++++++++++++++++++++++++++++-- backend/static/sw.js | 2 +- 7 files changed, 185 insertions(+), 10 deletions(-) diff --git a/backend/database.py b/backend/database.py index c56a2f6..cff0f89 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2075,6 +2075,18 @@ def _migrate(conn_factory): _seed_help_articles(conn) logger.info("Migration: Hilfe/FAQ-Tabelle bereit.") + conn.executescript(""" + CREATE TABLE IF NOT EXISTS bday_ki_cache ( + dog_id INTEGER NOT NULL, + year INTEGER NOT NULL, + mode TEXT NOT NULL, -- 'tomorrow' | 'today' + content TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')), + PRIMARY KEY (dog_id, year, mode) + ); + """) + logger.info("Migration: bday_ki_cache Tabelle bereit.") + # ---- Feature: Subscription-Tier ---- try: conn.execute("ALTER TABLE users ADD COLUMN subscription_tier TEXT DEFAULT 'standard'") diff --git a/backend/main.py b/backend/main.py index 78ab952..598438a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -327,7 +327,7 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") os.makedirs(MEDIA_DIR, exist_ok=True) app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") -APP_VER = "779" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "780" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/ki.py b/backend/routes/ki.py index 80d663c..f43f87e 100644 --- a/backend/routes/ki.py +++ b/backend/routes/ki.py @@ -167,6 +167,85 @@ def _log_rasse_request(user_id: int): ) +# ------------------------------------------------------------------ +# POST /ki/geburtstag — Geburtstags-Überraschungsideen (kostenlos für alle) +# ------------------------------------------------------------------ + +class BirthdayRequest(BaseModel): + dog_id: int + name: str + rasse: Optional[str] = None + alter: Optional[int] = None + mode: str = "tomorrow" # "tomorrow" | "today" + +@router.post("/geburtstag") +async def ki_geburtstag(req: BirthdayRequest, request: Request, + user=Depends(get_current_user)): + """Kostenlose KI-Geburtstagsideen — kein Premium nötig, 1x/Tag, DB-gecacht.""" + from datetime import date + year = date.today().year + mode = req.mode if req.mode in ("tomorrow", "today") else "tomorrow" + + # Aus DB-Cache zurückgeben wenn bereits generiert + with db() as conn: + cached = conn.execute( + "SELECT content FROM bday_ki_cache WHERE dog_id=? AND year=? AND mode=?", + (req.dog_id, year, mode) + ).fetchone() + if cached: + return {"answer": cached["content"], "cached": True} + + # Rate-Limit: 1 Generierung pro Tag pro User + rl_check(request, max_requests=1, window_seconds=86400, key=f"ki_bday_{req.dog_id}_{mode}") + + name = req.name.strip()[:40] or "deinen Hund" + rasse = req.rasse or None + alter = req.alter + rasse_str = f"({rasse})" if rasse else "" + + if mode == "today": + # Aus Sicht des Hundes — was er sich für seinen Geburtstag vorstellt + alter_str = f"{alter}. Geburtstag" if alter else "Geburtstag" + system = ( + "Du bist ein Hund und erzählst aus deiner eigenen Perspektive. " + "Schreibe auf Deutsch, verspielt, liebevoll und mit Hundelogik. " + "Verwende typische Hundegedanken: Fressen, Gassi, Schmusen, Spielen, Gerüche." + ) + prompt = ( + f"Ich bin {name} {rasse_str} und heute ist mein {alter_str}! " + f"Erzähl in meiner Stimme (als Hund), wie ich mir den perfekten Geburtstagstag vorgestellt habe — " + f"von Morgen bis Abend. Was möchte ich erleben, fressen, riechen, spielen? " + f"Ca. 150 Wörter, herzlich und humorvoll." + ) + else: + # Überraschungsideen für morgen + alter_str = f"{alter}. Geburtstag" if alter else "Geburtstag" + system = ( + "Du bist ein begeisterter Hundefreund mit vielen kreativen Ideen. " + "Antworte auf Deutsch, herzlich, konkret und mit einer Prise Humor. " + "Fokus auf praktische, umsetzbare Überraschungen." + ) + prompt = ( + f"Morgen ist der {alter_str} von {name} {rasse_str}! " + f"Was können wir {name} besonders gönnen? " + f"Gib 5 konkrete, liebevolle Überraschungsideen — von einfach bis aufwendig, " + f"jeweils mit einem Satz warum Hunde das lieben." + ) + + try: + answer = await ki_module.complete( + system=system, prompt=prompt, max_tokens=600, requires_premium=False, + ) + with db() as conn: + conn.execute( + "INSERT OR REPLACE INTO bday_ki_cache (dog_id, year, mode, content) VALUES (?,?,?,?)", + (req.dog_id, year, mode, answer) + ) + return {"answer": answer, "cached": False} + except ki_module.KIUnavailableError: + raise HTTPException(503, "KI momentan nicht verfügbar.") + + # ------------------------------------------------------------------ # POST /ki/rasse-erkennung — Vision-basierte Rassenerkennung # ------------------------------------------------------------------ diff --git a/backend/static/index.html b/backend/static/index.html index bade895..66698b5 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -575,10 +575,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 8131af6..2c57217 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '779'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '780'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js index 6daaaf4..71018a3 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -1322,8 +1322,8 @@ window.Worlds = (() => { .bday-fw2 { display:inline-block; animation: bday-fw2 1.1s ease-in-out infinite .2s; } .bday-fw3 { display:inline-block; animation: bday-fw3 1.6s ease-in-out infinite .4s; } -
🎆 @@ -1341,7 +1341,20 @@ window.Worlds = (() => { ${bdayYear ? `
${bday === 'today' ? `${bdayYear} Jahr${bdayYear !== 1 ? 'e' : ''} gemeinsam 🐾` : `Wird ${bdayYear} Jahr${bdayYear !== 1 ? 'e' : ''} alt`}
` : ''} -
` : ''} +
+ ${bday === 'today' ? '🐾 Was hat sich Ban Yaro gewünscht? →' : '✨ KI-Überraschungsideen →'} +
+
+ ${bday === 'today' && new Date().getHours() >= 18 ? ` +
+ + + + + Halte den besonderen Tag im Tagebuch fest 🐾 + +
` : ''}` : ''}
${!_hasBgPhoto ? ` @@ -1391,6 +1404,77 @@ window.Worlds = (() => { if (!isNaN(idx) && idx !== _dogIdx) { _dogIdx = idx; _renderHund(); } }); }); + + // Geburtstags-Banner → KI + el.querySelector('#wh-bday-banner')?.addEventListener('click', () => _openBdayKI(dog, bday)); + // Abend-Banner: nach 18 Uhr am echten Geburtstag → Tagebucheintrag anregen + if (bday === 'today' && new Date().getHours() >= 18) { + el.querySelector('#wh-bday-evening')?.addEventListener('click', () => { + navigateTo('diary'); + setTimeout(() => window.App?.callModule?.('diary', 'openNew'), 400); + }); + } + } + + async function _openBdayKI(dog, bdayMode) { + const isToday = bdayMode === 'today'; + const title = isToday ? `🎂 ${_esc(dog.name)}s Geburtstagstraum` : `🎁 Überraschungen für ${_esc(dog.name)}`; + + const ov = document.createElement('div'); + ov.className = 'w3-sheet-overlay'; + ov.innerHTML = ` +
+
+
+
${title}
+ +
+
+
+ + KI denkt nach… +
+
+ ${isToday ? ` + ` : ''} +
`; + document.body.appendChild(ov); + const _close = () => ov.remove(); + ov.querySelector('.w3-backdrop').addEventListener('click', _close); + ov.querySelector('#bday-ki-close').addEventListener('click', _close); + ov.querySelector('#bday-diary-btn')?.addEventListener('click', () => { + _close(); + navigateTo('diary'); + setTimeout(() => window.App?.callModule?.('diary', 'openNew'), 400); + }); + + try { + const res = await API.post('/ki/geburtstag', { + dog_id: dog.id, + name: dog.name, + rasse: dog.rasse || null, + alter: dog.alter_jahre ? Math.round(dog.alter_jahre) : null, + mode: bdayMode, + }); + const body = ov.querySelector('#bday-ki-body'); + if (body) { + const html = _esc(res.answer || '') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\n/g, '
'); + body.innerHTML = `
${html}
`; + } + } catch { + const body = ov.querySelector('#bday-ki-body'); + if (body) body.innerHTML = '
KI momentan nicht verfügbar 🐾
'; + } } // ── WELT WORLD ─────────────────────────────────────────────── diff --git a/backend/static/sw.js b/backend/static/sw.js index 28f026d..d0b1bf3 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v779'; +const CACHE_VERSION = 'by-v780'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache