diff --git a/.gitignore b/.gitignore index c08ec73..28e4c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ __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 89c141e..01dfc17 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1236,11 +1236,16 @@ 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 NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_id INTEGER 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 89e9065..188056f 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 or not ANTHROPIC_KEY: + if not user_id: return False try: from database import db @@ -173,8 +173,10 @@ async def complete( raise except Exception as e: logger.warning(f"Cloud-KI nicht erreichbar für privilegierten User, Fallback lokal: {e}") - # Fallback auf lokales Modell - text = await _local_complete(prompt, system, max_tokens, json_mode) + 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 _track_usage(user_id, "local") if return_model: return (text, LOCAL_MODEL) @@ -399,7 +401,7 @@ async def health_summary(health_data: list, dog_info: dict, if not subset: return " (keine Einträge)" lines = [] - for e in subset[:10]: # maximal 10 pro Typ + for e in subset[:5]: # maximal 5 pro Typ — Kontextfenster schonen 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 4a9ee01..c014419 100644 --- a/backend/main.py +++ b/backend/main.py @@ -140,6 +140,20 @@ 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. @@ -327,7 +341,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 = "785" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "819" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): @@ -1544,6 +1558,84 @@ 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 96743e0..f803b07 100644 --- a/backend/routes/health.py +++ b/backend/routes/health.py @@ -454,7 +454,21 @@ async def ki_zusammenfassung(dog_id: int, user=Depends(get_current_user)): user_is_premium=bool(user.get("is_premium")), user_id=user["id"], ) - return {"zusammenfassung": result} + 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} 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 708d571..de82240 100644 --- a/backend/routes/ki.py +++ b/backend/routes/ki.py @@ -232,6 +232,7 @@ 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( diff --git a/backend/routes/profile.py b/backend/routes/profile.py index 9cb0667..783951f 100644 --- a/backend/routes/profile.py +++ b/backend/routes/profile.py @@ -142,3 +142,28 @@ 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/design-system.css b/backend/static/css/design-system.css index 8683ab0..12bbb8f 100644 --- a/backend/static/css/design-system.css +++ b/backend/static/css/design-system.css @@ -8,6 +8,7 @@ 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 8b0f233..38c5fdc 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -3,7 +3,7 @@ - + @@ -67,6 +67,9 @@ } + + + @@ -82,20 +85,25 @@ Ban Yaro - + - - - + + + @@ -575,10 +583,10 @@ - - - - + + + + @@ -625,20 +633,36 @@ window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' }) .then(reg => { - // iOS PWA: Update sofort prüfen (Standalone-Modus prüft sonst nicht automatisch) + 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); reg.update(); }) .catch(err => console.warn('SW Registration failed:', err)); }); - // iOS PWA: erneut prüfen wenn App aus dem Hintergrund kommt + // Backup: erneut prüfen wenn App aus dem Hintergrund kommt document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { navigator.serviceWorker.getRegistration().then(reg => reg?.update()); } }); - // Wenn neuer SW die Kontrolle übernimmt → Seite neu laden + // Backup: controllerchange (falls updatefound nicht feuert) 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 1071fdd..893f1b4 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -45,6 +45,13 @@ 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 d7377de..11ea5b8 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 = '785'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '819'; // ← 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,6 +119,18 @@ 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 @@ -536,6 +548,7 @@ const App = (() => { if (window.Worlds) window.Worlds.init(state); _showVerifyBanner(); + _showAndroidBetaBanner(); _updateNotifBadge(); _updateChatBadge(); _checkNearbyAlerts(); @@ -612,11 +625,34 @@ const App = (() => { function _applyUserTheme(user) { const theme = user?.preferred_theme; - if (!theme || theme === 'system') return; // System-Einstellung: nichts tun + if (!theme || theme === 'system') { _syncThemeColor(); return; } 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() { @@ -857,6 +893,7 @@ 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] || ''; @@ -876,6 +913,18 @@ 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(); @@ -977,122 +1026,8 @@ const App = (() => { } // ---------------------------------------------------------- - // ---------------------------------------------------------- - // 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); - } - } + // VERSION-CHECK — stilles Auto-Update beim nächsten Seitenwechsel + function _initVersionCheck() { /* X-App-Version Header in api.js übernimmt das */ } // ---------------------------------------------------------- // ÖFFENTLICHE API diff --git a/backend/static/js/pages/health.js b/backend/static/js/pages/health.js index bec107e..91f082f 100644 --- a/backend/static/js/pages/health.js +++ b/backend/static/js/pages/health.js @@ -2312,11 +2312,14 @@ window.Page_health = (() => { // ---------------------------------------------------------- // KI-GESUNDHEITSBERICHTE (gespeicherte automatische Berichte) // ---------------------------------------------------------- - async function _loadKiBerichte(dogId) { + async function _loadKiBerichte(dogId, force = false) { const el = _container.querySelector('#health-ki-berichte'); if (!el) return; try { - const berichte = await API.health.kiBerichte(dogId); + // 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); if (!berichte || berichte.length === 0) return; const neuester = berichte[0]; const datum = neuester.erstellt_at @@ -2343,19 +2346,34 @@ window.Page_health = (() => { ${berichte.length > 1 ? `
${berichte.length} Berichte gespeichert — zum Öffnen tippen
` : ''} `; el.querySelector('.health-ki-bericht-banner').addEventListener('click', () => { - 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, - }); + 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(); }); } catch (_) { // Silently ignore — Berichte sind optional @@ -2790,11 +2808,16 @@ window.Page_health = (() => { UI.setLoading(btn, true); try { - const { zusammenfassung } = await API.health.kiZusammenfassung(_appState.activeDog.id); + 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 }); 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 b772d0f..9eed700 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -287,6 +287,15 @@ window.Page_settings = (() => { Abmelden + @@ -320,6 +329,15 @@ window.Page_settings = (() => { + ${/SamsungBrowser/i.test(navigator.userAgent) ? ` +
+ Samsung Internet Tipps:
+ • Farben: Einstellungen → Webseitenansicht → Dark Mode deaktivieren.
+ • Vollbild: Einstellungen → Display → Navigationsleiste → Wischgesten aktivieren. +
` : ''}
@@ -789,6 +807,24 @@ 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 38f9f08..9e554fe 100644 --- a/backend/static/js/pages/uebungen.js +++ b/backend/static/js/pages/uebungen.js @@ -479,6 +479,7 @@ 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 a4a550b..7ec576a 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 - if (!seen) { + // Banner beim ersten Besuch (nicht wenn defaultClosed gesetzt) + if (!seen && !config.defaultClosed) { 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 33fb509..f23f983 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:none"> + user-select:none;-webkit-tap-highlight-color:transparent;touch-action:pan-y"> ${!c.pinned ? `