From 163a6ff6a5d5f8f8d6d3cfd05342b8d0fc102503 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 13:01:51 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Fix:=20upgrade=5Frequests=20Query=20in=20ac?= =?UTF-8?q?tion=5Fitems=20try/except=20=E2=80=94=20sch=C3=BCtzt=20bestehen?= =?UTF-8?q?de=20Action=20Items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/admin.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 69dceeb..b8cfb40 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -124,9 +124,12 @@ async def action_items(user=Depends(require_mod)): users_today = conn.execute( "SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')" ).fetchone()[0] - upgrades_pending = conn.execute( - "SELECT COUNT(*) FROM upgrade_requests WHERE fulfilled_at IS NULL" - ).fetchone()[0] + try: + upgrades_pending = conn.execute( + "SELECT COUNT(*) FROM upgrade_requests WHERE fulfilled_at IS NULL" + ).fetchone()[0] + except Exception: + upgrades_pending = 0 return { "jobs_pending": jobs, "breeder_pending": breeders, From 60e5c9ba355162f67a29d13d079a765720e70d76 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 13:08:52 +0200 Subject: [PATCH 2/4] =?UTF-8?q?Fix:=20Admin-Endpoints=20umgehen=20SW-Timeo?= =?UTF-8?q?ut=20=E2=80=94=20kein=20'Offline'=20bei=20langen=20Jobs=20(SW?= =?UTF-8?q?=20by-v940)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/static/js/app.js | 2 +- backend/static/sw.js | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index bed3d6f..482806f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "939" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "940" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/js/app.js b/backend/static/js/app.js index cd79577..12d4361 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 = '939'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '940'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/sw.js b/backend/static/sw.js index cf6d85e..8241dc5 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v939'; +const CACHE_VERSION = 'by-v940'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache @@ -238,6 +238,8 @@ self.addEventListener('fetch', event => { // Media-Uploads + KI-Endpoints: direkt ans Netzwerk — kein Clone, kein Timeout, kein Queue // KI-Anfragen ans lokale LLM können mehrere Minuten dauern if (method === 'POST' && (_isMediaUpload(event.request) || url.pathname.startsWith('/api/ki/') || url.pathname.includes('/health/ki-') || url.pathname.includes('/health/symptom'))) return; + // Admin-Endpoints: kein SW-Timeout, direkt ans Netzwerk + if (url.pathname.startsWith('/api/admin/')) return; // Mutationen (POST/PATCH/PUT/DELETE): mit Timeout, bei Offline → Queue if (['POST', 'PATCH', 'PUT', 'DELETE'].includes(method)) { From 6762efa04e0a88488b76fa14d858d5423ebeb34c Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 13:17:23 +0200 Subject: [PATCH 3/4] =?UTF-8?q?UX:=20Upgrade-Freischaltung=20=E2=80=94=20r?= =?UTF-8?q?eichhaltige=20HTML-Best=C3=A4tigungsmail=20(SW=20by-v941)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/routes/admin.py | 88 +++++++++++++++++++++++++++++++++++----- backend/static/js/app.js | 2 +- backend/static/sw.js | 2 +- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/backend/main.py b/backend/main.py index 482806f..3ce196a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "940" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "941" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/admin.py b/backend/routes/admin.py index b8cfb40..09ab0b8 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -1150,19 +1150,87 @@ async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)): tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"} tier_label = tier_labels.get(req["tier"], req["tier"]) + tier_price = {"pro": "29 €/Jahr", "breeder": "49 €/Jahr"}.get(req["tier"], "") + + _features_pro = [ + ("users", "Mehrere Hunde", "Bis zu 10 Hunde mit getrennten Trainings-, Gesundheits- und Ernährungsdaten"), + ("fork-knife", "Ernährung", "Kalorienbedarf-Rechner, BARF-Guide, vollständige Giftliste, KI-Ernährungsberater"), + ("paw-print", "Gassi-Treffen", "Hundefotos & Rasse der Teilnehmer sichtbar, Fotos hochladen und teilen"), + ("chat-circle-dots", "Direktnachrichten & Chat", "Schreibe direkt mit anderen Hundebesitzern"), + ("handshake", "Playdate", "Spielkameraden in der Nähe finden und verabreden"), + ("airplane", "Reise-Checkliste", "Editierbare Checkliste + EU-Länder-Einreiseregeln"), + ("note-pencil", "Notizblock mit KI", "KI erkennt Muster in deinen Notizen und macht Vorschläge"), + ("map-trifold", "Erweiterte Karten-Layer", "Regenradar, Temperatur-Layer und weitere Kartenmodi"), + ] + _features_breeder = [ + ("check-circle", "Alle Pro-Features inklusive", "Mehrere Hunde, Ernährung, Gassi-Community, Chat, Playdate, Reise, Karten"), + ("tree-structure", "Zuchtkartei", "Gesundheitstests (HD, ED, OCD, Augen, Herz, Patella, ZTP), Gentests (MDR1, PRA, DM, vWD), Titel"), + ("notebook", "Wurfverwaltung", "Würfe und Welpen verwalten, Gewichtsverlauf, Fotos, Kaufvertrag automatisch ausfüllen"), + ("list-bullets", "Warteliste", "Interessenten mit Präferenzen pro Zuchthündin verwalten"), + ("thermometer", "Läufigkeit & Trächtigkeit", "Zykluskalender, Progesterontests, Deckdaten, automatische Meilensteinberechnung"), + ("graph", "Stammbaum & IK-Rechner", "Stammbaum bis 4 Generationen, Inzuchtkoeffizient nach Wright, Probeverpaarung"), + ("sparkle", "KI-Züchter-Assistent", "Wurfankündigungen schreiben, Genetik-Erklärung, Paarungsanalyse, Jahresbericht"), + ("globe", "Öffentliches Züchter-Profil", "Visitenkarte unter banyaro.app/breeder/{zwingername} mit Hunden, Fotos und Gesundheitsstatistik"), + ("download-simple", "Datenexport", "Vollständiger Export als HTML-Dossier und ODS-Tabelle (LibreOffice/Excel)"), + ] + + features = _features_breeder if req["tier"] == "breeder" else _features_pro + + def _feature_row(icon, title, desc): + return f""" + + +
+ + +
{title}
+
{desc}
+ + """ + + feature_rows = "".join(_feature_row(i, t, d) for i, t, d in features) + try: from mailer import send_email, email_html + import html as _html + name_esc = _html.escape(req["name"]) body_html = f""" -

Hallo {req['name']},

-

dein Account wurde soeben auf {tier_label} freigeschaltet.

-

Du kannst alle {tier_label}-Features ab sofort in der App nutzen. - Öffne Ban Yaro und lade die App einmal neu — dann ist dein neuer Tarif aktiv.

-

Vielen Dank für dein Vertrauen!

-

Viele Grüße
René & das Ban Yaro Team

""" - html = email_html(body_html, cta_url="https://banyaro.app", cta_label="Ban Yaro öffnen") - plain = (f"Hallo {req['name']},\n\ndein Account wurde auf {tier_label} freigeschaltet.\n" - f"Öffne Ban Yaro und lade die App neu.\n\nViele Grüße\nRené") - await send_email(req["email"], f"Dein {tier_label}-Zugang ist aktiv", html, plain) +

+ Herzlichen Glückwunsch, {name_esc}! 🎉 +

+

+ Dein Account wurde soeben auf {tier_label} + freigeschaltet. Vielen Dank für dein Vertrauen in Ban Yaro! +

+ +
+
Dein Tarif
+
{tier_label} + {tier_price} +
+
+ +
Deine neuen Features
+ + {feature_rows} +
+ +
+ So aktivierst du deine Features:
+ Öffne Ban Yaro und lade die App einmal neu (Startseite antippen → herunterziehen + oder App schließen und neu öffnen). Alle Features sind dann sofort verfügbar. +
""" + + html = email_html(body_html, cta_url="https://banyaro.app", cta_label="Ban Yaro jetzt öffnen") + plain = (f"Herzlichen Glückwunsch, {req['name']}!\n\n" + f"Dein Account wurde auf {tier_label} ({tier_price}) freigeschaltet.\n\n" + f"Öffne Ban Yaro und lade die App neu — alle Features sind dann aktiv.\n\n" + f"Viele Grüße\nRené & das Ban Yaro Team") + await send_email(req["email"], f"🎉 Dein {tier_label}-Zugang ist aktiv!", html, plain) except Exception as e: import logging logging.getLogger(__name__).warning(f"Bestätigungsmail fehlgeschlagen: {e}") diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 12d4361..d585465 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 = '940'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '941'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/sw.js b/backend/static/sw.js index 8241dc5..6803ce0 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v940'; +const CACHE_VERSION = 'by-v941'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache From 9da8665acad2ac1242ebc3149feb369bacd8a9d3 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 13:20:11 +0200 Subject: [PATCH 4/4] =?UTF-8?q?UX:=20Upgrade-Tab=20=E2=80=94=20Cards=20sta?= =?UTF-8?q?tt=20Tabelle,=20Freischalten-Button=20immer=20sichtbar=20(SW=20?= =?UTF-8?q?by-v942)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/pages/admin.js | 82 ++++++++++++++++++-------------- backend/static/sw.js | 2 +- 4 files changed, 49 insertions(+), 39 deletions(-) diff --git a/backend/main.py b/backend/main.py index 3ce196a..d145e80 100644 --- a/backend/main.py +++ b/backend/main.py @@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "941" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "942" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/js/app.js b/backend/static/js/app.js index d585465..b2802bd 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 = '941'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '942'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 00a19e8..34b7ccd 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -3519,55 +3519,65 @@ window.Page_admin = (() => { const pending = rows.filter(r => !r.fulfilled_at); const done = rows.filter(r => r.fulfilled_at); - const _row = (r, showBtn) => ` + // Offene Anfragen als Cards (mobile-freundlich, Button immer sichtbar) + const _pendingCard = r => ` +
+
+
+
${_esc(r.name)}
+
${_esc(r.email)}
+
+ ${tierBadge(r.tier)} + ${r.created_at?.slice(0,10) || ''} +
+ ${r.message ? `
+ ${_esc(r.message)} +
` : ''} +
+
+ +
`; + + // Erledigte als kompakte Tabellenzeilen + const _doneRow = r => ` - ${_esc(r.name)}
+ ${_esc(r.name)}
${_esc(r.email)} ${tierBadge(r.tier)} - - ${r.message ? _esc(r.message) : '—'} - - ${r.created_at?.slice(0,10) || ''} - - ${showBtn - ? `` - : `✓ ${r.fulfilled_at?.slice(0,10)}`} - + + ✓ ${r.fulfilled_at?.slice(0,10) || ''} `; - const thead = ` - ${['Nutzer','Tarif','Nachricht','Datum','Aktion'].map(h => - `${h}` - ).join('')}`; - el.innerHTML = ` -
-
Offene Anfragen (${pending.length})
-
- - ${thead} - - ${pending.length - ? pending.map(r => _row(r, true)).join('') - : ``} - -
- Keine offenen Anfragen -
+
+
+ Offene Anfragen (${pending.length})
+ ${pending.length + ? pending.map(_pendingCard).join('') + : `
+ Keine offenen Anfragen +
`}
${done.length ? `
Erledigt (${done.length})
- ${thead} - ${done.map(r => _row(r, false)).join('')} + + ${['Nutzer','Tarif','Freigegeben'].map(h => + `` + ).join('')} + + ${done.map(_doneRow).join('')}
${h}
` : ''}`; diff --git a/backend/static/sw.js b/backend/static/sw.js index 6803ce0..3404b58 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v941'; +const CACHE_VERSION = 'by-v942'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache