Compare commits
No commits in common. "34fe59a67297d0851581f2c0e36da1754ac4a03a" and "af7fe7813e466c359cb4c5c4d9d5ef097c529b1b" have entirely different histories.
34fe59a672
...
af7fe7813e
8 changed files with 10 additions and 188 deletions
|
|
@ -2075,18 +2075,6 @@ def _migrate(conn_factory):
|
||||||
_seed_help_articles(conn)
|
_seed_help_articles(conn)
|
||||||
logger.info("Migration: Hilfe/FAQ-Tabelle bereit.")
|
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 ----
|
# ---- Feature: Subscription-Tier ----
|
||||||
try:
|
try:
|
||||||
conn.execute("ALTER TABLE users ADD COLUMN subscription_tier TEXT DEFAULT 'standard'")
|
conn.execute("ALTER TABLE users ADD COLUMN subscription_tier TEXT DEFAULT 'standard'")
|
||||||
|
|
|
||||||
|
|
@ -327,7 +327,7 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||||
os.makedirs(MEDIA_DIR, exist_ok=True)
|
os.makedirs(MEDIA_DIR, exist_ok=True)
|
||||||
app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
|
app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
|
||||||
|
|
||||||
APP_VER = "781" # muss mit APP_VER in app.js übereinstimmen
|
APP_VER = "779" # muss mit APP_VER in app.js übereinstimmen
|
||||||
|
|
||||||
@app.get("/.well-known/assetlinks.json")
|
@app.get("/.well-known/assetlinks.json")
|
||||||
async def assetlinks():
|
async def assetlinks():
|
||||||
|
|
|
||||||
|
|
@ -167,82 +167,6 @@ 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}
|
|
||||||
|
|
||||||
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
|
# POST /ki/rasse-erkennung — Vision-basierte Rassenerkennung
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -575,10 +575,10 @@
|
||||||
<div id="modal-container"></div>
|
<div id="modal-container"></div>
|
||||||
|
|
||||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||||
<script src="/js/api.js?v=781"></script>
|
<script src="/js/api.js?v=779"></script>
|
||||||
<script src="/js/ui.js?v=781"></script>
|
<script src="/js/ui.js?v=779"></script>
|
||||||
<script src="/js/app.js?v=781"></script>
|
<script src="/js/app.js?v=779"></script>
|
||||||
<script src="/js/worlds.js?v=781"></script>
|
<script src="/js/worlds.js?v=779"></script>
|
||||||
|
|
||||||
<!-- Feature-Seiten werden lazy geladen -->
|
<!-- Feature-Seiten werden lazy geladen -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '781'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '779'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt
|
||||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||||
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
||||||
|
|
|
||||||
|
|
@ -1322,8 +1322,8 @@ window.Worlds = (() => {
|
||||||
.bday-fw2 { display:inline-block; animation: bday-fw2 1.1s ease-in-out infinite .2s; }
|
.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; }
|
.bday-fw3 { display:inline-block; animation: bday-fw3 1.6s ease-in-out infinite .4s; }
|
||||||
</style>
|
</style>
|
||||||
<div class="world-reminder bday-pop" id="wh-bday-banner" style="flex-direction:column;align-items:center;
|
<div class="world-reminder bday-pop" style="flex-direction:column;align-items:center;
|
||||||
text-align:center;gap:6px;padding:14px 16px;cursor:pointer;
|
text-align:center;gap:6px;padding:14px 16px;
|
||||||
background:rgba(0,0,0,0.42);border-color:rgba(196,132,58,0.6)">
|
background:rgba(0,0,0,0.42);border-color:rgba(196,132,58,0.6)">
|
||||||
<div style="display:flex;gap:6px;font-size:1.5rem;line-height:1">
|
<div style="display:flex;gap:6px;font-size:1.5rem;line-height:1">
|
||||||
<span class="bday-fw1">🎆</span>
|
<span class="bday-fw1">🎆</span>
|
||||||
|
|
@ -1341,20 +1341,7 @@ window.Worlds = (() => {
|
||||||
${bdayYear ? `<div style="font-size:10px;color:rgba(255,255,255,0.55)">
|
${bdayYear ? `<div style="font-size:10px;color:rgba(255,255,255,0.55)">
|
||||||
${bday === 'today' ? `${bdayYear} Jahr${bdayYear !== 1 ? 'e' : ''} gemeinsam 🐾` : `Wird ${bdayYear} Jahr${bdayYear !== 1 ? 'e' : ''} alt`}
|
${bday === 'today' ? `${bdayYear} Jahr${bdayYear !== 1 ? 'e' : ''} gemeinsam 🐾` : `Wird ${bdayYear} Jahr${bdayYear !== 1 ? 'e' : ''} alt`}
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
<div style="font-size:10px;color:rgba(196,132,58,0.9);font-weight:700;margin-top:2px">
|
</div>` : ''}
|
||||||
${bday === 'today' ? '🐾 Was hat sich Ban Yaro gewünscht? →' : '✨ KI-Überraschungsideen →'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${bday === 'today' && new Date().getHours() >= 18 ? `
|
|
||||||
<div class="world-reminder" id="wh-bday-evening" style="cursor:pointer;
|
|
||||||
background:rgba(0,0,0,0.32);border-color:rgba(196,132,58,0.4)">
|
|
||||||
<svg class="ph-icon" style="width:1.1rem;height:1.1rem;color:var(--c-primary)">
|
|
||||||
<use href="/icons/phosphor.svg#book-open"></use>
|
|
||||||
</svg>
|
|
||||||
<span style="font-size:var(--text-xs);font-weight:700;color:rgba(255,255,255,0.85)">
|
|
||||||
Halte den besonderen Tag im Tagebuch fest 🐾
|
|
||||||
</span>
|
|
||||||
</div>` : ''}` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="world-bottom">
|
<div class="world-bottom">
|
||||||
${!_hasBgPhoto ? `
|
${!_hasBgPhoto ? `
|
||||||
|
|
@ -1404,82 +1391,6 @@ window.Worlds = (() => {
|
||||||
if (!isNaN(idx) && idx !== _dogIdx) { _dogIdx = idx; _renderHund(); }
|
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 = `
|
|
||||||
<div class="w3-backdrop"></div>
|
|
||||||
<div class="w3-sheet-panel w3-sheet-panel--scroll">
|
|
||||||
<div class="w3-sheet-header w3-sheet-header--mb20">
|
|
||||||
<div class="w3-sheet-title">${title}</div>
|
|
||||||
<button id="bday-ki-close" class="w3-close-btn">
|
|
||||||
<svg class="ph-icon" style="width:14px;height:14px"><use href="/icons/phosphor.svg#x"></use></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="bday-ki-body" style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7">
|
|
||||||
<div style="display:flex;align-items:center;gap:10px;color:var(--c-text-secondary);padding:var(--space-4) 0">
|
|
||||||
<svg class="ph-icon" style="width:20px;height:20px"><use href="/icons/phosphor.svg#sparkle"></use></svg>
|
|
||||||
KI denkt nach…
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${isToday ? `
|
|
||||||
<button id="bday-diary-btn" style="margin-top:var(--space-4);width:100%;display:flex;align-items:center;
|
|
||||||
justify-content:center;gap:var(--space-2);padding:var(--space-3);border-radius:var(--radius-md);
|
|
||||||
border:1px solid var(--c-primary);background:transparent;color:var(--c-primary);
|
|
||||||
font-size:var(--text-sm);font-weight:600;cursor:pointer">
|
|
||||||
<svg class="ph-icon" style="width:16px;height:16px"><use href="/icons/phosphor.svg#book-open"></use></svg>
|
|
||||||
Besonderen Tagebucheintrag anlegen
|
|
||||||
</button>` : ''}
|
|
||||||
</div>`;
|
|
||||||
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, '<strong>$1</strong>')
|
|
||||||
.replace(/\n/g, '<br>');
|
|
||||||
body.innerHTML = `<div style="padding-bottom:var(--space-2)">${html}</div>`;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const body = ov.querySelector('#bday-ki-body');
|
|
||||||
if (body) {
|
|
||||||
const msg = err?.status === 429
|
|
||||||
? 'Heute bereits abgerufen — morgen gibt es neue Ideen 🐾'
|
|
||||||
: 'KI momentan nicht verfügbar. Versuch es später nochmal 🐾';
|
|
||||||
body.innerHTML = `<div style="color:var(--c-text-secondary)">${msg}</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── WELT WORLD ───────────────────────────────────────────────
|
// ── WELT WORLD ───────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v781';
|
const CACHE_VERSION = 'by-v779';
|
||||||
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
|
||||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ services:
|
||||||
- DB_PATH=/data/banyaro.db
|
- DB_PATH=/data/banyaro.db
|
||||||
- MEDIA_DIR=/prod-media
|
- MEDIA_DIR=/prod-media
|
||||||
- STAGING=true
|
- STAGING=true
|
||||||
- KI_MODE=cloud
|
|
||||||
- VAPID_PUBLIC_KEY=BMKbFAmpsqJ-eFef_4XJcYpuxPWqBNAoy9buMNnMSa6ijcPzltboHi_YccPKJrUD0isBez-vJIzAgjnLTWkzcC0
|
- VAPID_PUBLIC_KEY=BMKbFAmpsqJ-eFef_4XJcYpuxPWqBNAoy9buMNnMSa6ijcPzltboHi_YccPKJrUD0isBez-vJIzAgjnLTWkzcC0
|
||||||
- VAPID_PRIVATE_KEY=8PWa9vvwMqtqsJEJGcwmiLhR0_Yl7duVX3wmWiKS878
|
- VAPID_PRIVATE_KEY=8PWa9vvwMqtqsJEJGcwmiLhR0_Yl7duVX3wmWiKS878
|
||||||
- VAPID_CONTACT=mailto:admin@banyaro.app
|
- VAPID_CONTACT=mailto:admin@banyaro.app
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue