-"""
- 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.
+
+
+
+
+
+
+
+
+ ${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);
+ }
+ }
// ----------------------------------------------------------
// Ö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 ? `
-