diff --git a/.gitignore b/.gitignore index 28e4c9f..c08ec73 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,3 @@ __pycache__/ # Design-Quell-Dateien (nicht für Server) /icons/ .claude/worktrees/ -Ban Yaro - Google Play package/ diff --git a/backend/database.py b/backend/database.py index 01dfc17..89c141e 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1236,16 +1236,11 @@ def _migrate(conn_factory): CREATE TABLE IF NOT EXISTS ki_health_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, bericht TEXT NOT NULL, erstellt_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) - # user_id nachträglich ergänzen falls Tabelle ohne diese Spalte erstellt wurde - try: - conn.execute("ALTER TABLE ki_health_reports ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE") - except Exception: - pass # Spalte existiert bereits conn.execute(""" CREATE INDEX IF NOT EXISTS idx_ki_health_reports_dog ON ki_health_reports(dog_id, erstellt_at DESC) diff --git a/backend/ki.py b/backend/ki.py index 188056f..89e9065 100644 --- a/backend/ki.py +++ b/backend/ki.py @@ -84,7 +84,7 @@ def _track_usage(user_id: int | None, source: str) -> None: def _is_cloud_priority_user(user_id: int | None) -> bool: """Privilegierte Rollen (Admin, Moderator, Züchter, Manager) nutzen Cloud-KI primär.""" - if not user_id: + if not user_id or not ANTHROPIC_KEY: return False try: from database import db @@ -173,10 +173,8 @@ async def complete( raise except Exception as e: logger.warning(f"Cloud-KI nicht erreichbar für privilegierten User, Fallback lokal: {e}") - try: - text = await _local_complete(prompt, system, max_tokens, json_mode) - except Exception as local_e: - raise KIUnavailableError("KI-Modell nicht erreichbar.") from local_e + # Fallback auf lokales Modell + text = await _local_complete(prompt, system, max_tokens, json_mode) _track_usage(user_id, "local") if return_model: return (text, LOCAL_MODEL) @@ -401,7 +399,7 @@ async def health_summary(health_data: list, dog_info: dict, if not subset: return " (keine Einträge)" lines = [] - for e in subset[:5]: # maximal 5 pro Typ — Kontextfenster schonen + for e in subset[:10]: # maximal 10 pro Typ line = f" - {e.get('datum', '?')}: {e.get('bezeichnung', '?')}" if e.get("naechstes"): line += f" (nächste Fälligkeit: {e['naechstes']})" diff --git a/backend/main.py b/backend/main.py index 3ebd1fa..4a9ee01 100644 --- a/backend/main.py +++ b/backend/main.py @@ -140,20 +140,6 @@ class _UploadSizeMiddleware(BaseHTTPMiddleware): app.add_middleware(_UploadSizeMiddleware) -class _AppVersionMiddleware(BaseHTTPMiddleware): - """Fügt X-App-Version zu allen /api/-Antworten hinzu. - api.js erkennt damit sofort wenn eine neue Version deployed wurde - und lädt beim nächsten Seitenwechsel automatisch neu — kein Banner nötig. - """ - async def dispatch(self, request: Request, call_next): - response = await call_next(request) - if request.url.path.startswith('/api/'): - response.headers['X-App-Version'] = APP_VER - return response - -app.add_middleware(_AppVersionMiddleware) - - class _CacheControlMiddleware(BaseHTTPMiddleware): """Setzt Cache-Control-Header für statische Assets. CSS/JS: no-cache (ETag-Validierung) — iOS cached sonst ewig ohne Ablaufdatum. @@ -341,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 = "826" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "785" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): @@ -1558,84 +1544,6 @@ async def presse(): return FileResponse(f"{STATIC_DIR}/presse.html", headers={"Cache-Control": "max-age=3600"}) -@app.get("/konto-loeschen") -async def konto_loeschen(): - from fastapi.responses import HTMLResponse - html = """ - - - - - Konto löschen — Ban Yaro - - - - -

← Zurück zu Ban Yaro

-

Konto löschen

-

Du kannst dein Ban Yaro-Konto und alle zugehörigen Daten dauerhaft löschen.

-
- ⚠️ Diese Aktion ist unwiderruflich. Alle Daten (Tagebuch, Gesundheit, Training, Fotos) werden dauerhaft gelöscht. -
-

So löschst du dein Konto:

-
    -
  1. Öffne banyaro.app und melde dich an
  2. -
  3. Tippe auf das Menü-Symbol oben rechts
  4. -
  5. Gehe zu Einstellungen
  6. -
  7. Scrolle nach unten zu „Konto löschen"
  8. -
  9. Bestätige die Löschung
  10. -
- Ban Yaro öffnen -

- Alternativ kannst du die Löschung per E-Mail an - support@banyaro.app beantragen. -

- -""" - return HTMLResponse(content=html, headers={"Cache-Control": "max-age=3600"}) - - -# /force-update — SW + Cache-Killer für hartnäckige alte Versionen -# ------------------------------------------------------------------ -@app.get("/force-update") -async def force_update(): - from fastapi.responses import HTMLResponse - html = """ - -Ban Yaro — Update - - -
⏳ Aktualisiere Ban Yaro…
-

Service Worker wird entfernt…

-""" - return HTMLResponse(content=html, headers={"Cache-Control": "no-store"}) - - # /partner — Influencer-Landingpage # ------------------------------------------------------------------ @app.get("/partner") diff --git a/backend/routes/health.py b/backend/routes/health.py index f803b07..96743e0 100644 --- a/backend/routes/health.py +++ b/backend/routes/health.py @@ -454,21 +454,7 @@ async def ki_zusammenfassung(dog_id: int, user=Depends(get_current_user)): user_is_premium=bool(user.get("is_premium")), user_id=user["id"], ) - save_error = None - try: - with db() as conn: - conn.execute( - "INSERT INTO ki_health_reports (dog_id, user_id, bericht) VALUES (?,?,?)", - (dog_id, user["id"], result) - ) - count = conn.execute( - "SELECT COUNT(*) FROM ki_health_reports WHERE dog_id=?", (dog_id,) - ).fetchone()[0] - except Exception as e: - save_error = str(e) - count = 0 - logger.warning(f"KI-Bericht konnte nicht gespeichert werden: {e}") - return {"zusammenfassung": result, "saved_count": count, "save_error": save_error} + return {"zusammenfassung": result} except KIPremiumRequired as e: raise HTTPException(402, str(e)) except KIUnavailableError as e: diff --git a/backend/routes/ki.py b/backend/routes/ki.py index 6521b90..708d571 100644 --- a/backend/routes/ki.py +++ b/backend/routes/ki.py @@ -232,7 +232,6 @@ async def ki_geburtstag(req: BirthdayRequest, request: Request, try: answer = await ki_module.complete( system=system, prompt=prompt, max_tokens=600, requires_premium=False, - user_id=user["id"], ) with db() as conn: conn.execute( @@ -276,11 +275,11 @@ async def ki_rasse_erkennung( # Rate-Limit prüfen remaining_before = _check_rasse_limit(user["id"]) - # Anthropic-Key zur Laufzeit prüfen (nicht nur beim Modulstart) - import os as _os - api_key = _os.getenv("ANTHROPIC_KEY") or ki_module.ANTHROPIC_KEY - if not api_key: + # Anthropic-Client holen (nutzt cached Instanz aus ki.py) + if not ki_module.ANTHROPIC_KEY: raise HTTPException(503, "KI-Bildanalyse ist momentan nicht verfügbar.") + + api_key = ki_module.ANTHROPIC_KEY base64_data = base64.standard_b64encode(content).decode("utf-8") prompt_text = """Analysiere dieses Bild und erkenne die Hunderasse(n). diff --git a/backend/routes/profile.py b/backend/routes/profile.py index 783951f..9cb0667 100644 --- a/backend/routes/profile.py +++ b/backend/routes/profile.py @@ -142,28 +142,3 @@ async def put_world_config(body: WorldConfigIn, user=Depends(get_current_user)): conn.execute("UPDATE users SET world_config=? WHERE id=?", (_json.dumps(body.config), user['id'])) return {"status": "ok"} - - -# ---------------------------------------------------------- -# DELETE /profile/account — Konto unwiderruflich löschen -# ---------------------------------------------------------- -@router.delete('/account') -async def delete_account(user=Depends(get_current_user)): - """Löscht das Konto und alle zugehörigen Daten unwiderruflich.""" - uid = user['id'] - with db() as conn: - # Alle Hunde-IDs des Users - dog_ids = [r['id'] for r in conn.execute( - "SELECT id FROM dogs WHERE user_id=?", (uid,)).fetchall()] - for did in dog_ids: - conn.execute("DELETE FROM diary WHERE dog_id=?", (did,)) - conn.execute("DELETE FROM health WHERE dog_id=?", (did,)) - conn.execute("DELETE FROM training_sessions WHERE dog_id=?", (did,)) - conn.execute("DELETE FROM training_streaks WHERE dog_id=?", (did,)) - conn.execute("DELETE FROM expenses WHERE dog_id=?", (did,)) - conn.execute("DELETE FROM dogs WHERE user_id=?", (uid,)) - conn.execute("DELETE FROM push_subscriptions WHERE user_id=?", (uid,)) - conn.execute("DELETE FROM notifications WHERE user_id=?", (uid,)) - conn.execute("DELETE FROM forum_posts WHERE user_id=?", (uid,)) - conn.execute("DELETE FROM users WHERE id=?", (uid,)) - return {"status": "deleted"} diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 49d89ab..71b87ca 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -3087,15 +3087,13 @@ html.modal-open { } @media (min-width: 768px) { .map-full-layout { top: 0; left: var(--nav-sidebar-width); bottom: 0; } - /* Zoom-Control und Filter-Tabs unter die Statusleiste schieben */ - .map-full-layout .leaflet-top { padding-top: 28px; } } .map-full { width: 100%; height: 100%; } /* Legende: horizontaler Scroll-Strip oben */ .map-legend { position: absolute; - top: 28px; /* mind. Status-Leisten-Höhe auf Tablet/iPad */ + top: var(--space-2); left: 42px; /* Zoom-Control (+/-) freilassen */ right: 0; z-index: 1000; diff --git a/backend/static/css/design-system.css b/backend/static/css/design-system.css index 12bbb8f..8683ab0 100644 --- a/backend/static/css/design-system.css +++ b/backend/static/css/design-system.css @@ -8,7 +8,6 @@ 1. TOKENS — Farben, Abstände, Typografie, Schatten ------------------------------------------------------------ */ :root { - color-scheme: dark light; /* Primärfarben — Honig-Amber aus Ban Yaros Fell */ --c-primary: #C4843A; --c-primary-dark: #9E6520; diff --git a/backend/static/index.html b/backend/static/index.html index cf2117f..8b0f233 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -3,7 +3,7 @@ - + @@ -67,9 +67,6 @@ } - - - @@ -85,25 +82,20 @@ Ban Yaro - + - - - + + + @@ -583,10 +575,10 @@ - - - - + + + + @@ -633,36 +625,20 @@ window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' }) .then(reg => { - function _watchSW(sw) { - if (!sw) return; - sw.addEventListener('statechange', () => { - if (sw.state === 'activated') { - // Kein zweiter Reload nach force-update - if (sessionStorage.getItem('by_skip_sw_reload')) { - sessionStorage.removeItem('by_skip_sw_reload'); - return; - } - window.location.replace('/?_t=' + Date.now()); - } - }); - } - // Listener VOR update() registrieren — verhindert Race Condition - reg.addEventListener('updatefound', () => _watchSW(reg.installing)); - // Falls SW bereits installiert (Seite wurde nach SW-Install neu geladen) - if (reg.installing) _watchSW(reg.installing); + // iOS PWA: Update sofort prüfen (Standalone-Modus prüft sonst nicht automatisch) reg.update(); }) .catch(err => console.warn('SW Registration failed:', err)); }); - // Backup: erneut prüfen wenn App aus dem Hintergrund kommt + // iOS PWA: erneut prüfen wenn App aus dem Hintergrund kommt document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { navigator.serviceWorker.getRegistration().then(reg => reg?.update()); } }); - // Backup: controllerchange (falls updatefound nicht feuert) + // Wenn neuer SW die Kontrolle übernimmt → Seite neu laden navigator.serviceWorker.addEventListener('controllerchange', () => { window.location.replace('/?_t=' + Date.now()); }); diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 893f1b4..1071fdd 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -45,13 +45,6 @@ const API = (() => { throw new APIError(msg, 0, 'network'); } - // Versions-Check: Server meldet neue Version → Banner anzeigen (einmalig) - const serverVer = response.headers.get('x-app-version'); - if (serverVer && serverVer !== APP_VER && !window._byUpdatePending) { - window._byUpdatePending = true; - window._byNewVersion = serverVer; - } - if (response.status === 204) return null; let data; diff --git a/backend/static/js/app.js b/backend/static/js/app.js index db6b183..d7377de 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 = '826'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '785'; // ← 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 @@ -119,18 +119,6 @@ const App = (() => { // ---------------------------------------------------------- function navigate(pageId, pushHistory = true, params = {}) { if (!pages[pageId]) return; - // Neue Version erkannt → nur aktualisieren wenn kein Bearbeitungsfenster offen ist - if (window._byUpdatePending) { - const modalOpen = document.querySelector('#modal-container .modal-overlay') !== null; - if (!modalOpen) { - window._byUpdatePending = false; - sessionStorage.setItem('by_updated_to', window._byNewVersion || ''); - sessionStorage.setItem('by_update_target', pageId); // Zielseite nach Update - location.href = '/force-update'; - return; - } - // Modal offen → beim nächsten Seitenwechsel versuchen - } if (window.Worlds?._visible) window.Worlds.hide(); // Aktive Seite ausblenden @@ -548,7 +536,6 @@ const App = (() => { if (window.Worlds) window.Worlds.init(state); _showVerifyBanner(); - _showAndroidBetaBanner(); _updateNotifBadge(); _updateChatBadge(); _checkNearbyAlerts(); @@ -625,34 +612,11 @@ const App = (() => { function _applyUserTheme(user) { const theme = user?.preferred_theme; - if (!theme || theme === 'system') { _syncThemeColor(); return; } + if (!theme || theme === 'system') return; // System-Einstellung: nichts tun localStorage.setItem('by_theme', theme); const html = document.documentElement; if (theme === 'dark') html.setAttribute('data-theme', 'dark'); else if (theme === 'light') html.setAttribute('data-theme', 'light'); - _syncThemeColor(); - } - - function _syncThemeColor() { - const isAndroid = /android/i.test(navigator.userAgent); - const isDark = isAndroid - || document.documentElement.getAttribute('data-theme') === 'dark' - || (window.matchMedia('(prefers-color-scheme: dark)').matches - && document.documentElement.getAttribute('data-theme') !== 'light'); - document.getElementById('meta-theme-color')?.setAttribute('content', isDark ? '#0f1623' : '#C4843A'); - } - - function _showAndroidBetaBanner() { - // Nur auf Android, nur einmalig, nur für eingeloggte Nutzer - if (!/android/i.test(navigator.userAgent)) return; - if (localStorage.getItem('by_android_beta_dismissed')) return; - setTimeout(() => { - UI.toast.info( - '📱 Play Store Beta: Hilf uns beim Android-Test! Schreib an support@banyaro.app', - 20000 - ); - localStorage.setItem('by_android_beta_dismissed', '1'); - }, 5000); } function _showVerifyBanner() { @@ -893,7 +857,6 @@ const App = (() => { // INITIALISIERUNG // ---------------------------------------------------------- async function init() { - _syncThemeColor(); // Statusleisten-Farbe sofort setzen // Spezielle Hash-Parameter → in App bleiben (kein /info-Redirect) const _rawHash = location.hash.replace('#', ''); const _hashQuery = _rawHash.split('?')[1] || ''; @@ -913,18 +876,6 @@ const App = (() => { _bindNavigation(); - // Nach stillem Update: Toast + zur ursprünglichen Zielseite navigieren - const updatedTo = sessionStorage.getItem('by_updated_to'); - if (updatedTo) { - sessionStorage.removeItem('by_updated_to'); - const target = sessionStorage.getItem('by_update_target'); - sessionStorage.removeItem('by_update_target'); - setTimeout(() => { - UI.toast?.success(`App auf v${updatedTo} aktualisiert`); - if (target && pages[target]) navigate(target, false); - }, 800); - } - try { localStorage.removeItem('by_wissen_open'); } catch (_) {} _initVersionCheck(); @@ -1026,8 +977,122 @@ const App = (() => { } // ---------------------------------------------------------- - // VERSION-CHECK — stilles Auto-Update beim nächsten Seitenwechsel - function _initVersionCheck() { /* X-App-Version Header in api.js übernimmt das */ } + // ---------------------------------------------------------- + // VERSION-CHECK — persistentes Banner wenn neue Version verfügbar + // ---------------------------------------------------------- + let _updateBannerShown = false; + + async function _checkVersion() { + try { + const r = await fetch('/api/version', { cache: 'no-store' }); + if (!r.ok) return; + const { version } = await r.json(); + if (version && version !== APP_VER && !_updateBannerShown) { + _updateBannerShown = true; + _showUpdateBanner(version); + } + } catch { /* offline — ignorieren */ } + } + + function _showUpdateBanner(newVersion) { + const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent); + const existing = document.getElementById('app-update-banner'); + if (existing) return; + + const banner = document.createElement('div'); + banner.id = 'app-update-banner'; + banner.style.cssText = [ + 'position:fixed;bottom:calc(env(safe-area-inset-bottom,0px) + 72px);left:12px;right:12px', + 'z-index:9000;background:var(--c-primary);color:#fff;border-radius:16px', + 'padding:14px 16px;box-shadow:0 4px 20px rgba(0,0,0,0.3)', + 'display:flex;flex-direction:column;gap:10px', + ].join(';'); + + banner.innerHTML = ` +
+
+
+ Neue Version verfügbar (v${newVersion}) +
+
+ Tippe auf Aktualisieren um die neueste Version zu laden. +
+
+
+ + +
+
+ + `; + + document.body.appendChild(banner); + + banner.querySelector('#upd-btn-close').addEventListener('click', () => banner.remove()); + + banner.querySelector('#upd-btn-reload').addEventListener('click', async () => { + const btn = banner.querySelector('#upd-btn-reload'); + btn.textContent = 'Lädt…'; + btn.disabled = true; + sessionStorage.setItem('by_update_reload', APP_VER); + // ?_t= Timestamp zwingt iOS bfcache zur Aufgabe — wird beim Start sofort entfernt + setTimeout(() => location.replace('/?_t=' + Date.now()), 800); + try { + const reg = await navigator.serviceWorker?.getRegistration(); + if (reg?.waiting) reg.waiting.postMessage({ type: 'SKIP_WAITING' }); + reg?.update().catch(() => {}); // kein await — kann hängen + const keys = await caches.keys(); + await Promise.all(keys.map(k => caches.delete(k))); + } catch { /* ignorieren */ } + }); + } + + function _initVersionCheck() { + // Beim Start nach 10 Sekunden prüfen (nicht sofort — Prio für Auth) + setTimeout(_checkVersion, 10_000); + // Dann alle 30 Minuten + setInterval(_checkVersion, 30 * 60_000); + // Beim Wiedereinstieg in die App + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') _checkVersion(); + }); + // Nach Reload: war das ein Update-Reload? Falls Version immer noch alt → iOS-Hinweis + const reloadVer = sessionStorage.getItem('by_update_reload'); + if (reloadVer && reloadVer === APP_VER) { + // Version hat sich nicht geändert nach Reload → iOS-Cache-Problem + sessionStorage.removeItem('by_update_reload'); + setTimeout(() => { + fetch('/api/version', { cache: 'no-store' }) + .then(r => r.json()) + .then(({ version }) => { + if (version && version !== APP_VER) { + _updateBannerShown = true; + _showUpdateBanner(version); + // iOS-Hinweis sofort aufklappen + setTimeout(() => { + document.getElementById('upd-ios-hint')?.style.setProperty('display', 'block'); + }, 300); + } + }).catch(() => {}); + }, 2000); + } + } // ---------------------------------------------------------- // ÖFFENTLICHE API diff --git a/backend/static/js/pages/health.js b/backend/static/js/pages/health.js index 91f082f..bec107e 100644 --- a/backend/static/js/pages/health.js +++ b/backend/static/js/pages/health.js @@ -2312,14 +2312,11 @@ window.Page_health = (() => { // ---------------------------------------------------------- // KI-GESUNDHEITSBERICHTE (gespeicherte automatische Berichte) // ---------------------------------------------------------- - async function _loadKiBerichte(dogId, force = false) { + async function _loadKiBerichte(dogId) { const el = _container.querySelector('#health-ki-berichte'); if (!el) return; try { - // force=true: Cache-Buster damit SW den neuen Bericht nicht übersieht - const berichte = force - ? await API.get(`/dogs/${dogId}/health/ki-berichte?_t=${Date.now()}`) - : await API.health.kiBerichte(dogId); + const berichte = await API.health.kiBerichte(dogId); if (!berichte || berichte.length === 0) return; const neuester = berichte[0]; const datum = neuester.erstellt_at @@ -2346,34 +2343,19 @@ window.Page_health = (() => { ${berichte.length > 1 ? `
${berichte.length} Berichte gespeichert — zum Öffnen tippen
` : ''} `; el.querySelector('.health-ki-bericht-banner').addEventListener('click', () => { - let idx = 0; - const fmtDate = b => b.erstellt_at - ? new Date(b.erstellt_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) - : ''; - - function showBericht() { - const b = berichte[idx]; - const nav = berichte.length > 1 ? ` -
- - ${idx+1} / ${berichte.length} - -
` : ''; - UI.modal.open({ - title: `${UI.icon('star')} KI-Gesundheitsberichte`, - body: `${nav} -
${fmtDate(b)}
-
${_esc(b.bericht)}
`, - }); - } - - window._kiPrev = () => { if (idx < berichte.length - 1) { idx++; showBericht(); } }; - window._kiNext = () => { if (idx > 0) { idx--; showBericht(); } }; - showBericht(); + const listeHtml = berichte.map((b, i) => { + const d = b.erstellt_at + ? new Date(b.erstellt_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) + : ''; + return `
+ ${d ? `
${d}
` : ''} +
${_esc(b.bericht)}
+
`; + }).join(''); + UI.modal.open({ + title: `${UI.icon('star')} KI-Gesundheitsberichte`, + body: listeHtml, + }); }); } catch (_) { // Silently ignore — Berichte sind optional @@ -2808,16 +2790,11 @@ window.Page_health = (() => { UI.setLoading(btn, true); try { - const res = await API.health.kiZusammenfassung(_appState.activeDog.id); - const zusammenfassung = res.zusammenfassung ?? res; - if (res.save_error) UI.toast.warning(`Speichern fehlgeschlagen: ${res.save_error}`); - else if (res.saved_count !== undefined) UI.toast.info(`${res.saved_count} Bericht(e) in DB`, { duration: 8000 }); + const { zusammenfassung } = await API.health.kiZusammenfassung(_appState.activeDog.id); UI.modal.open({ title: `${UI.icon('star')} KI-Gesundheitsbericht`, body: `
${_esc(zusammenfassung)}
`, }); - // Berichte-Liste nach Generierung frisch laden (Cache-Buster) - _loadKiBerichte(_appState.activeDog.id, true); } catch (err) { if (err.status === 503) { UI.toast.error('KI ist momentan nicht verfügbar. Bitte später erneut versuchen.'); diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 9eed700..b772d0f 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -287,15 +287,6 @@ window.Page_settings = (() => { Abmelden - @@ -329,15 +320,6 @@ window.Page_settings = (() => { - ${/SamsungBrowser/i.test(navigator.userAgent) ? ` -
- Samsung Internet Tipps:
- • Farben: Einstellungen → Webseitenansicht → Dark Mode deaktivieren.
- • Vollbild: Einstellungen → Display → Navigationsleiste → Wischgesten aktivieren. -
` : ''}
@@ -807,24 +789,6 @@ window.Page_settings = (() => { _render(); }); - document.getElementById('settings-delete-account-btn')?.addEventListener('click', async () => { - const ok = await UI.modal.confirm({ - title: 'Konto unwiderruflich löschen?', - body: 'Alle deine Daten (Tagebuch, Gesundheit, Training, Fotos) werden dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.', - confirmText: 'Ja, Konto löschen', - danger: true, - }); - if (!ok) return; - try { - await API.del('/profile/account'); - _appState.user = null; _appState.dogs = []; _appState.activeDog = null; - UI.toast.info('Dein Konto wurde gelöscht.'); - App.navigate('welcome'); - } catch { - UI.toast.error('Konto konnte nicht gelöscht werden. Bitte versuche es erneut.'); - } - }); - document.getElementById('settings-install-btn')?.addEventListener('click', () => { App.navigate('welcome', true, { install: true }); }); diff --git a/backend/static/js/pages/uebungen.js b/backend/static/js/pages/uebungen.js index 9e554fe..38f9f08 100644 --- a/backend/static/js/pages/uebungen.js +++ b/backend/static/js/pages/uebungen.js @@ -479,7 +479,6 @@ window.Page_uebungen = (() => { _render(); _helpHandle = UI.pageInfo(_container, { pageId: 'uebungen', - defaultClosed: true, title: 'Übungsbibliothek', icon: 'graduation-cap', intro: 'Hier findest du alle Übungen für deinen Hund — von Grundkommandos bis zu Tricks und Problemverhalten. Du kannst deinen Trainingsfortschritt für jede Übung festhalten.', diff --git a/backend/static/js/ui.js b/backend/static/js/ui.js index 7ec576a..a4a550b 100644 --- a/backend/static/js/ui.js +++ b/backend/static/js/ui.js @@ -316,8 +316,8 @@ const UI = (() => { // Kein automatischer absolut-positionierter Trigger mehr. // Aufrufer kann openModal() nutzen und den Button selbst platzieren. - // Banner beim ersten Besuch (nicht wenn defaultClosed gesetzt) - if (!seen && !config.defaultClosed) { + // Banner beim ersten Besuch + if (!seen) { localStorage.setItem(seenKey, '1'); const banner = document.createElement('div'); banner.className = 'pinfo-banner'; diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js index f23f983..33fb509 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -741,7 +741,7 @@ window.Worlds = (() => { border-radius:16px;padding:10px 4px 8px;height:80px;box-sizing:border-box; display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px; cursor:grab;position:relative;min-width:0;overflow:hidden; - user-select:none;-webkit-tap-highlight-color:transparent;touch-action:pan-y"> + user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none"> ${!c.pinned ? `