+"""
+ 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.
-
-
-
-
-
-
-
-
- ${isIos
- ? `Falls die App nach dem Aktualisieren noch die alte Version zeigt:
- 1. Drücke lange auf das App-Icon am Homescreen
- 2. Wähle „App entfernen" (nur das Symbol, keine Daten)
- 3. Öffne banyaro.app in Safari und füge die App erneut hinzu`
- : `Falls die Seite noch die alte Version zeigt:
- Drücke Cmd+Shift+R (Mac) bzw. Ctrl+Shift+R (Windows/Android Chrome) für einen harten Reload.`}
-
- `;
-
- 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