From eaa2e02e888babb9d855c2eafec8ff843eb6f0e5 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 09:43:21 +0200 Subject: [PATCH 01/26] =?UTF-8?q?SEO:=20llms.txt=20v1.5.1=20+=20sitemap=20?= =?UTF-8?q?/zuechter=20+=20JSON-LD=20Pricing=20(Pro=2029=E2=82=AC/Z=C3=BCc?= =?UTF-8?q?hter=2049=E2=82=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - llms.txt: Dual-Audience-Positionierung, echte Preise, neue Züchter-Features (Warteliste, Läufigkeit, Wurf-Buchstabe/-Name, Privater Header, Profilfotos), neue URL /zuechter, SW by-v918, Datum 2026-05-14 - landing.html JSON-LD: 3 Offers (kostenlos/Pro 29€/Züchter 49€), 7 neue featureList-Einträge, dateModified 2026-05-14, Beschreibung mit Preisen - zuechter.html JSON-LD: 2 Offers (49€/39€ Gründer), 5 neue Features, dateModified + softwareVersion - sitemap.xml: neue statische Datei (Backup-Referenz, dynamic route in main.py) - main.py sitemap: /zuechter mit priority 0.9 hinzugefügt --- backend/main.py | 7 ++-- backend/static/landing.html | 43 +++++++++++++++++++------ backend/static/llms.txt | 62 +++++++++++++++++++++++++----------- backend/static/sitemap.xml | 46 ++++++++++++++++++++++++++ backend/static/zuechter.html | 27 ++++++++++++++-- 5 files changed, 153 insertions(+), 32 deletions(-) create mode 100644 backend/static/sitemap.xml diff --git a/backend/main.py b/backend/main.py index 097edd7..93009ed 100644 --- a/backend/main.py +++ b/backend/main.py @@ -465,10 +465,11 @@ async def sitemap(): today = date.today().isoformat() urls = [ ("https://banyaro.app/", "weekly", "1.0"), - ("https://banyaro.app/info", "monthly", "0.9"), - ("https://banyaro.app/presse", "monthly", "0.8"), + ("https://banyaro.app/zuechter", "weekly", "0.9"), + ("https://banyaro.app/info", "monthly", "0.8"), + ("https://banyaro.app/presse", "monthly", "0.7"), ("https://banyaro.app/wiki/rassen", "weekly", "0.8"), - ("https://banyaro.app/knigge", "monthly", "0.8"), + ("https://banyaro.app/knigge", "monthly", "0.7"), ("https://banyaro.app/wurfboerse", "daily", "0.8"), ] diff --git a/backend/static/landing.html b/backend/static/landing.html index 768eade..68f7607 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -31,19 +31,38 @@ "@type": "MobileApplication", "name": "Ban Yaro", "alternateName": "Ban Yaro — Die Hunde-Plattform", - "description": "Ban Yaro ist die kostenlose, deutschsprachige All-in-One Hunde-App für Hundebesitzer und Züchter. Tagebuch, Impfpass, Wurfbörse, Stammbaum, Inzucht-Koeffizient, Tierschutz-Check, Giftköder-Alarm, Gassi-Community — DSGVO-konform, ohne App Store.", + "description": "Ban Yaro ist die deutschsprachige All-in-One Hunde-Plattform für Hundebesitzer und Züchter. Kostenlos: Tagebuch, Impfpass, Gassi-Community, Giftköder-Alarm. Pro (29 €/Jahr): mehrere Hunde, Ernährung. Züchter (49 €/Jahr): Warteliste, Läufigkeit, Wurfverwaltung, Stammbaum, Inzucht-Koeffizient, KI-Assistent — DSGVO-konform, ohne App Store.", "url": "https://banyaro.app", "applicationCategory": "LifestyleApplication", "applicationSubCategory": "PetApplication", "operatingSystem": "iOS, Android, Web", "inLanguage": "de", "availableOnDevice": "Smartphone, Tablet", - "offers": { - "@type": "Offer", - "price": "0", - "priceCurrency": "EUR", - "availability": "https://schema.org/InStock" - }, + "offers": [ + { + "@type": "Offer", + "name": "Kostenlos", + "price": "0", + "priceCurrency": "EUR", + "availability": "https://schema.org/InStock" + }, + { + "@type": "Offer", + "name": "Ban Yaro Pro", + "price": "29", + "priceCurrency": "EUR", + "availability": "https://schema.org/InStock", + "description": "Mehrere Hunde, Ernährung, erweiterte Karten-Layer" + }, + { + "@type": "Offer", + "name": "Züchter", + "price": "49", + "priceCurrency": "EUR", + "availability": "https://schema.org/InStock", + "description": "Vollständige Züchter-Plattform: Warteliste, Läufigkeit, Wurfverwaltung, Stammbaum, IK-Rechner, KI-Assistent" + } + ], "publisher": { "@type": "Organization", "name": "Ban Yaro", @@ -108,12 +127,18 @@ "DSGVO Datenexport (Art. 20): vollständiger JSON-Download aller eigenen Daten", "Hunde-Persönlichkeitstest mit Trainingstipps", "Reise-Checkliste und EU-Länder-Einreiseregeln", - "Integrierte Hilfe und FAQ ohne App Store" + "Integrierte Hilfe und FAQ ohne App Store", + "Warteliste: Interessenten mit Präferenzen pro Zuchthündin verwalten", + "Läufigkeit und Trächtigkeit: Zykluskalender, Progesterontests, Deckdaten, Meilensteine", + "Wurf-Buchstabe und Wurf-Name für jeden Wurf", + "Privater Züchter-Bereich mit Logo und Zwingername im Header", + "Züchter-Profilfotos und Galerie auf der öffentlichen Visitenkarte", + "Züchter-Kacheln in HUND-Welt mit Z-Badge erkennbar" ], "screenshot": "https://banyaro.app/icons/icon-512.png", "softwareVersion": "1.5.1", "datePublished": "2026-05-01", - "dateModified": "2026-05-12", + "dateModified": "2026-05-14", "areaServed": ["DE", "AT", "CH"], "audience": { "@type": "Audience", diff --git a/backend/static/llms.txt b/backend/static/llms.txt index 65c7a1c..bbc78bd 100644 --- a/backend/static/llms.txt +++ b/backend/static/llms.txt @@ -1,6 +1,6 @@ -# Ban Yaro — Die deutschsprachige Hunde-Plattform +# Ban Yaro — Die deutschsprachige Hunde-Plattform für Hundebesitzer und Züchter # https://banyaro.app -# Letzte Aktualisierung: 2026-05-12 +# Letzte Aktualisierung: 2026-05-14 ## Was ist Ban Yaro? @@ -10,7 +10,11 @@ Kein App Store, kein US-Konzern, DSGVO-konform, selbst gehostet in Deutschland. Das Kern-Versprechen: "Alles rund um deinen Hund — von Welpe bis Opa." -Ban Yaro ist kostenlos nutzbar (Freemium-Modell). Die App ist auf allen Smartphones installierbar +Ban Yaro richtet sich an zwei Zielgruppen, die nahtlos verzahnt sind: +- **Hundebesitzer**: vollständige Alltags-App (Tagebuch, Gesundheit, Training, Gassi-Community) +- **Züchter**: professionelles Zucht-Management direkt in derselben App (Warteliste, Läufigkeit, Wurf, Stammbaum) + +Ban Yaro ist im Freemium-Modell nutzbar. Die App ist auf allen Smartphones installierbar (iOS und Android) direkt über den Browser — ohne App Store. ## Der Name „Ban Yaro" @@ -33,12 +37,12 @@ gegründet, mit eigenem Schutzrecht auf den Namen. - Keine Werbung, keine Datenweitergabe an Dritte, kein Tracking (Umami, cookieless) - Kontakt: hallo@banyaro.app - Keine App-Store-Abhängigkeit: Als PWA direkt installierbar, keine Gatekeeper -- Aktuelle Version: v1.5.1 (Mai 2026), SW by-v885 +- Aktuelle Version: v1.5.1 (Mai 2026), SW by-v918 ## Zielgruppe - Deutschsprachige Hundebesitzer (Deutschland, Österreich, Schweiz) -- Verantwortungsvolle Hundezüchter (VDH und andere Verbände) +- Verantwortungsvolle Hundezüchter (VDH und andere Verbände) — dedizierte Landing Page: https://banyaro.app/zuechter - Welpen-Interessenten und Käufer - Hundeschulen und Hundetrainer - Tierärzte und Praxen @@ -182,21 +186,10 @@ Die Startseite für eingeloggte Nutzer zeigt: - KI lokal: LM Studio (Gemma-4-31B) - KI Cloud: Claude API (claude-sonnet-4-6, Anthropic) -## Monetarisierung - -**Kostenlos:** -- Alle Basis-Features inkl. Züchter-Antrag, Wurfverwaltung, Stammbaum, Tierschutz-Check - -**Züchter-Provision** (geplant): Wurfbörse bleibt für Käufer kostenlos - -**Ban Yaro Plus** (ca. 4,99 €/Monat, in Entwicklung): -- KI-Trainingsplan, erweiterte Statistiken - -**Hundesitting**: 8% Provision - ## Öffentliche Seiten (ohne Login) -- https://banyaro.app — Landing Page +- https://banyaro.app — Landing Page (Hundebesitzer + Züchter) +- https://banyaro.app/zuechter — Dedizierte Landing Page für Züchter - https://banyaro.app/info — Landing Page (Alias) - https://banyaro.app/wiki/rassen — Alle Hunderassen - https://banyaro.app/wiki/rasse/{slug} — Rassen-Detail @@ -250,6 +243,39 @@ Die Startseite für eingeloggte Nutzer zeigt: - **Auth-geschützte Medien**: Tagebuch-, Gesundheits- und Gassi-Fotos sind nur für eingeloggte Nutzer abrufbar — kein öffentlicher Zugriff über URL möglich. - **Datenschutzerklärung v2**: Vollständige Transparenz über KI-Datenübertragungen (Gesundheitsdaten im Cloud-Prompt, Fotos bei Rassenerkennung), OpenWeatherMap und Nominatim ergänzt, Datenexport konkret beschrieben. +## Features ab v1.5.1 — Züchter-Plattform Vollausbau (Mai 2026) + +- **Warteliste**: Interessenten mit Präferenzen (Geschlecht, Farbe, Verwendungszweck) pro Zuchthündin verwalten — mit Status (Interessent / Reserviert / Abgesagt) und Kontaktdaten. +- **Läufigkeit & Trächtigkeit**: Vollständiger Zykluskalender mit Progesterontests (Datum, ng/mL, Labormethode), Deckdaten (Rüde, Methode, Datum) und automatischer Meilensteinberechnung (Geburt, Absetzen, 8-Wochen-Abgabe). +- **Wurf-Buchstabe und -Name**: Jeder Wurf hat einen Rang-Buchstaben (A-Wurf, B-Wurf…) und optional einen freien Namen (z.B. "Vatertags-Wurf"). +- **Privater Züchter-Bereich**: Wurfverwaltung und Zuchtkartei zeigen Züchter-Logo und Zwingername im Header — professionelle, vertrauliche Arbeitsatmosphäre statt generischer App-Ansicht. +- **Züchter-Profilfotos**: Galerie direkt im Züchter-Profil — Fotos hochladen und Reihenfolge verwalten, öffentlich sichtbar auf der Profil-Visitenkarte. +- **Züchter-Profil als Visitenkarte**: Hero-Bereich mit Hintergrund, Hunde-Cards mit HD/ED-Ergebnis-Badges, Gesundheitsstatistik, Fotogalerie — öffentlich abrufbar unter banyaro.app/breeder/{zwingername}. +- **Dedizierte Züchter-Landing-Page**: https://banyaro.app/zuechter mit Erklärung aller Züchter-Features und Pricing. +- **Züchter-Kacheln in HUND-Welt**: Läufigkeit, Wurfverwaltung und Zuchtkartei sind als eigene Kacheln in der HUND-Navigation eingebunden — erkennbar am Z-Badge für Züchter-Features. + +## Monetarisierung + +**Kostenlos (dauerhaft):** +- Alle Basis-Features für Hundebesitzer: Tagebuch, Gesundheit, Gassi, Community, Forum, Wissen +- Züchter-Antrag, Wurfbörse, Stammbaum-Ansicht, Tierschutz-Check, Symptom-Checker +- 1 Hund + +**Ban Yaro Pro — 29 €/Jahr:** +- Mehrere Hunde +- Ernährungsbereich (KI-Berater, BARF-Guide) +- Erweiterte Karten-Layer +- Alle künftigen Pro-Features + +**Züchter-Abo — 49 €/Jahr** (Gründer-Preis: **39 €/Jahr** für die ersten 20 Züchter): +- Gesamte Züchter-Plattform: Wurfverwaltung, Zuchtkartei, Stammbaum, IK-Rechner +- Warteliste, Läufigkeit & Trächtigkeit, Kaufvertrag-Generator +- KI-Züchter-Assistenz (Wurfankündigungen, Paarungsanalyse, Jahresbericht) +- Datenexport (HTML + ODS) +- Verifiziertes Züchter-Profil mit öffentlicher Seite + +**Hundesitting**: 8% Provision (im Vergleich: Rover/Pawshake 20%) + ## Domains - https://banyaro.app (primäre Domain) diff --git a/backend/static/sitemap.xml b/backend/static/sitemap.xml new file mode 100644 index 0000000..b1cdd7a --- /dev/null +++ b/backend/static/sitemap.xml @@ -0,0 +1,46 @@ + + + + + https://banyaro.app/ + 2026-05-14 + weekly + 1.0 + + + + https://banyaro.app/zuechter + 2026-05-14 + weekly + 0.9 + + + + https://banyaro.app/wiki/rassen + 2026-05-14 + monthly + 0.8 + + + + https://banyaro.app/wurfboerse + 2026-05-14 + daily + 0.8 + + + + https://banyaro.app/knigge + 2026-05-01 + monthly + 0.6 + + + + https://banyaro.app/presse + 2026-05-14 + monthly + 0.6 + + + diff --git a/backend/static/zuechter.html b/backend/static/zuechter.html index c3ac3b8..e7e6b39 100644 --- a/backend/static/zuechter.html +++ b/backend/static/zuechter.html @@ -21,14 +21,37 @@ "@context": "https://schema.org", "@type": "SoftwareApplication", "name": "Ban Yaro — Züchter-Tool", - "description": "Digitales Zucht-Management für seriöse Hundezüchter: Stammbaum, Inzuchtkoeffizient nach Wright, Gesundheitstests, Gentests, Wurfverwaltung, Kaufvertrag-Generator, KI-Assistent.", + "description": "Professionelles Zucht-Management für seriöse Hundezüchter direkt in der Ban Yaro App: Warteliste, Läufigkeit und Trächtigkeit, Wurfverwaltung, Stammbaum, Inzuchtkoeffizient, Gesundheitstests, Gentests, KI-Assistent. Ab 49 €/Jahr, Gründer-Preis 39 €/Jahr.", "url": "https://banyaro.app/zuechter", "applicationCategory": "BusinessApplication", "operatingSystem": "iOS, Android, Web", "inLanguage": "de", - "offers": { "@type": "Offer", "price": "0", "priceCurrency": "EUR" }, + "offers": [ + { + "@type": "Offer", + "name": "Züchter-Abo", + "price": "49", + "priceCurrency": "EUR", + "availability": "https://schema.org/InStock", + "description": "Vollständige Züchter-Plattform" + }, + { + "@type": "Offer", + "name": "Gründer-Preis (erste 20 Züchter)", + "price": "39", + "priceCurrency": "EUR", + "availability": "https://schema.org/LimitedAvailability" + } + ], "audience": { "@type": "Audience", "audienceType": "Hundezüchter, VDH-Züchter, Rassehundzüchter" }, + "softwareVersion": "1.5.1", + "dateModified": "2026-05-14", "featureList": [ + "Warteliste: Interessenten mit Präferenzen pro Zuchthündin verwalten", + "Läufigkeit und Trächtigkeit: Zykluskalender, Progesterontests, Deckdaten, automatische Meilensteine", + "Wurf-Buchstabe und Wurf-Name für jeden Wurf", + "Privater Bereich mit Züchter-Logo und Zwingername im Header", + "Züchter-Profilfotos und öffentliche Visitenkarte", "Stammbaum bis 4 Generationen grafisch", "Inzuchtkoeffizient nach Wright mit Ampel-Bewertung", "Probeverpaarung mit IK-Simulation", From d61fd155c5d15e2b4b00e6c036dc1150970f5a08 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 09:48:01 +0200 Subject: [PATCH 02/26] =?UTF-8?q?Feature:=20Abo=20&=20Tarif=20in=20Einstel?= =?UTF-8?q?lungen=20=E2=80=94=20Upgrade-UI=20f=C3=BCr=20Pro=20+=20Z=C3=BCc?= =?UTF-8?q?hter=20(SW=20by-v919)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /api/me gibt subscription_tier jetzt zurück (fehlte im SELECT) - settings.js: "Pro kommt bald" durch echte Abo-Karte ersetzt - Zeigt aktuellen Tarif mit farbigem Badge (Kostenlos/Pro/Züchter/Admin) - Standard-Nutzer: zwei Upgrade-Buttons (Pro 29€/Jahr, Züchter 49€/Jahr) - Pro-Nutzer: Pro-Badge + optionaler Züchter-Upgrade - Züchter/Admin: Status-Badge, keine Upgrade-Buttons - Upgrade-Modal: Features-Liste + ehrlicher Hinweis auf manuelle Freischaltung + mailto-Button mit vorausgefülltem Betreff und Account-E-Mail - SW by-v919, APP_VER 919 --- backend/main.py | 2 +- backend/routes/auth.py | 2 +- backend/static/index.html | 8 +- backend/static/js/app.js | 2 +- backend/static/js/pages/settings.js | 135 ++++++++++++++++++++++++++-- backend/static/sw.js | 2 +- 6 files changed, 136 insertions(+), 15 deletions(-) diff --git a/backend/main.py b/backend/main.py index 93009ed..2588f0c 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 = "918" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "919" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/auth.py b/backend/routes/auth.py index a0174e6..9ad43fe 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -240,7 +240,7 @@ async def me(user=Depends(get_current_user)): profil_sichtbarkeit, avatar_url, created_at, is_founder, is_partner, founder_number, is_founder_pending, notes_ki_enabled, gassi_stunde_push, - preferred_theme + preferred_theme, subscription_tier FROM users WHERE id=?""", (user["id"],) ).fetchone() diff --git a/backend/static/index.html b/backend/static/index.html index 0e13659..d079f75 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -599,10 +599,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index c6d2189..3e3529d 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 = '918'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '919'; // ← 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/settings.js b/backend/static/js/pages/settings.js index 8767cc6..f95e235 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -77,6 +77,125 @@ window.Page_settings = (() => { } } + // ---------------------------------------------------------- + // ABO & TARIF + // ---------------------------------------------------------- + function _tierCard(u) { + const tier = u.subscription_tier || 'standard'; + const rolle = u.rolle || 'user'; + const isAdmin = rolle === 'admin' || rolle === 'moderator'; + const isPro = ['pro','pro_test'].includes(tier); + const isBreeder = ['breeder','breeder_test'].includes(tier) || rolle === 'breeder'; + const isStandard = !isAdmin && !isPro && !isBreeder; + + const _badge = (label, color) => + `${label}`; + + const _upgradeBtn = (id, label, price, color) => + ``; + + let statusHtml = ''; + let actionsHtml = ''; + + if (isAdmin) { + statusHtml = _badge('Admin', '#6366f1'); + } else if (isBreeder) { + statusHtml = _badge('Züchter aktiv', '#C4843A'); + } else if (isPro) { + statusHtml = _badge('Pro aktiv', '#16a34a'); + actionsHtml = ` +
+ ${_upgradeBtn('settings-upgrade-breeder-btn','Züchter werden','49 €/Jahr','#C4843A')} +
`; + } else { + statusHtml = _badge('Kostenlos', '#888'); + actionsHtml = ` +
+ ${_upgradeBtn('settings-upgrade-pro-btn','Ban Yaro Pro','29 €/Jahr','#16a34a')} + ${_upgradeBtn('settings-upgrade-breeder-btn','Züchter','49 €/Jahr','#C4843A')} +
`; + } + + return ` +
+
Abo & Tarif
+
+
+ Aktueller Tarif: + ${statusHtml} +
+ ${actionsHtml} +
+
`; + } + + function _showUpgradeModal(tier) { + const isPro = tier === 'pro'; + const label = isPro ? 'Ban Yaro Pro' : 'Züchter'; + const price = isPro ? '29 €/Jahr' : '49 €/Jahr'; + const features = isPro + ? ['Mehrere Hunde verwalten', 'Ernährungsbereich mit KI-Berater', 'Erweiterte Karten-Layer', 'Alle künftigen Pro-Features'] + : ['Vollständige Züchter-Plattform', 'Warteliste, Läufigkeit & Trächtigkeit', 'Wurfverwaltung, Stammbaum, IK-Rechner', 'KI-Züchter-Assistent & Datenexport']; + + const featureList = features.map(f => + `
  • ✓ ${f}
  • ` + ).join(''); + + const subject = encodeURIComponent(`Upgrade auf ${label} — Ban Yaro`); + const body = encodeURIComponent( + `Hallo,\n\nich möchte meinen Account auf ${label} upgraden.\n\nMein Account: ${_appState.user?.email || ''}\n\nBitte schickt mir die Zahlungsinformationen.\n\nViele Grüße` + ); + const mailHref = `mailto:hallo@banyaro.app?subject=${subject}&body=${body}`; + + UI.modal.open({ + title: `${label} freischalten`, + body: ` +
    +
    +
    ${price}
    +
    + Einmaliger Jahresbeitrag
    Kündigung jederzeit möglich +
    +
    +
      + ${featureList} +
    +
    + Aktuell läuft die Freischaltung noch manuell. Schreib uns kurz eine E-Mail — + wir schalten deinen Account innerhalb von 24 Stunden frei und schicken + dir die Bankverbindung. +
    +
    `, + footer: ` + + + E-Mail senden + ` + }); + } + // ---------------------------------------------------------- // RENDER // ---------------------------------------------------------- @@ -276,13 +395,6 @@ window.Page_settings = (() => { Feedback geben - ${!_appState.user?.subscription_tier || _appState.user.subscription_tier === 'standard' || _appState.user.subscription_tier === 'standard_test' ? ` -
    - ⭐ Ban Yaro Pro kommt bald — mehr Features, mehrere Hunde. -
    - ` : ''}
    ` + : `✓ ${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 +
    +
    +
    + ${done.length ? ` +
    +
    Erledigt (${done.length})
    +
    + + ${thead} + ${done.map(r => _row(r, false)).join('')} +
    +
    +
    ` : ''}`; + + el.querySelectorAll('.adm-fulfill-btn').forEach(btn => { + btn.addEventListener('click', async () => { + const { id, name, tier } = btn.dataset; + const tierLabel = { pro: 'Pro', breeder: 'Züchter' }[tier] || tier; + const ok = await UI.modal.confirm({ + title: `${name} auf ${tierLabel} freischalten?`, + body: `

    + Der Account wird auf ${tierLabel} gesetzt und + eine Bestätigungsmail gesendet. +

    `, + confirmLabel: 'Freischalten', + danger: false, + }); + if (!ok) return; + btn.disabled = true; + btn.textContent = '…'; + try { + const res = await API.post(`/admin/upgrade-requests/${id}/fulfill`); + UI.toast.success(`${res.user} wurde auf ${tierLabel} freigeschaltet.`); + _renderTab(); + } catch (e) { + UI.toast.error(e.message); + btn.disabled = false; + btn.textContent = 'Freischalten'; + } + }); + }); + } + return { init, refresh, onDogChange }; })(); diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index f95e235..96ee9f7 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -143,6 +143,7 @@ window.Page_settings = (() => { const isPro = tier === 'pro'; const label = isPro ? 'Ban Yaro Pro' : 'Züchter'; const price = isPro ? '29 €/Jahr' : '49 €/Jahr'; + const color = isPro ? '#16a34a' : '#C4843A'; const features = isPro ? ['Mehrere Hunde verwalten', 'Ernährungsbereich mit KI-Berater', 'Erweiterte Karten-Layer', 'Alle künftigen Pro-Features'] : ['Vollständige Züchter-Plattform', 'Warteliste, Läufigkeit & Trächtigkeit', 'Wurfverwaltung, Stammbaum, IK-Rechner', 'KI-Züchter-Assistent & Datenexport']; @@ -151,18 +152,12 @@ window.Page_settings = (() => { `
  • ✓ ${f}
  • ` ).join(''); - const subject = encodeURIComponent(`Upgrade auf ${label} — Ban Yaro`); - const body = encodeURIComponent( - `Hallo,\n\nich möchte meinen Account auf ${label} upgraden.\n\nMein Account: ${_appState.user?.email || ''}\n\nBitte schickt mir die Zahlungsinformationen.\n\nViele Grüße` - ); - const mailHref = `mailto:hallo@banyaro.app?subject=${subject}&body=${body}`; - UI.modal.open({ title: `${label} freischalten`, body: `
    -
    ${price}
    +
    ${price}
    Einmaliger Jahresbeitrag
    Kündigung jederzeit möglich
    @@ -173,9 +168,8 @@ window.Page_settings = (() => {
    - Aktuell läuft die Freischaltung noch manuell. Schreib uns kurz eine E-Mail — - wir schalten deinen Account innerhalb von 24 Stunden frei und schicken - dir die Bankverbindung. + Wir schalten deinen Account manuell frei — innerhalb von 24 Stunden. + Wir melden uns mit den Zahlungsdetails per E-Mail.
    `, footer: ` @@ -185,14 +179,32 @@ window.Page_settings = (() => { color:var(--c-text);font-size:var(--text-sm);cursor:pointer"> Abbrechen - - E-Mail senden - ` + ` + }); + + document.getElementById('upgrade-request-send-btn')?.addEventListener('click', async () => { + const btn = document.getElementById('upgrade-request-send-btn'); + if (!btn) return; + btn.disabled = true; + btn.textContent = 'Wird gesendet…'; + try { + const res = await API.auth.upgradeRequest(tier); + UI.modal.close(); + if (res.already) { + UI.toast.info('Deine Anfrage liegt bereits vor — wir melden uns bald.'); + } else { + UI.toast.success('Anfrage gesendet! Wir melden uns per E-Mail.'); + } + } catch (e) { + btn.disabled = false; + btn.textContent = 'Anfrage senden'; + UI.toast.error(e.message || 'Fehler beim Senden.'); + } }); } diff --git a/backend/static/sw.js b/backend/static/sw.js index dfad2b4..7fe3d87 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-v919'; +const CACHE_VERSION = 'by-v920'; 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 52160e4dc02df517e57a11474c9133a2001a6487 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 10:06:48 +0200 Subject: [PATCH 04/26] =?UTF-8?q?Fix:=20Admin=20Z=C3=BCchter-Tab=20?= =?UTF-8?q?=E2=80=94=20Alle=20Z=C3=BCchter=20Liste=20+=20Antraege-Section?= =?UTF-8?q?=20(SW=20by-v921)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/admin/breeders: neuer Endpunkt listet alle aktiven Züchter mit Zwingername, Rasse, Stadt, Würfe/Zuchthunde-Zähler, subscription_tier - _renderZuechter: zwei Sektionen parallel geladen - "Offene Anträge" (wie vorher, aber mit Section-Header auch wenn leer) - "Alle Züchter": Tabelle analog Nutzer-Tab mit Abo-Button → _changeTier - api.js: API.breeder.allList() hinzugefügt - SW by-v921, APP_VER 921 --- backend/main.py | 2 +- backend/routes/breeder.py | 21 +++++++ backend/static/js/api.js | 1 + backend/static/js/app.js | 2 +- backend/static/js/pages/admin.js | 95 +++++++++++++++++++++++++++++--- backend/static/sw.js | 2 +- 6 files changed, 113 insertions(+), 10 deletions(-) diff --git a/backend/main.py b/backend/main.py index 8e75ede..9f5a2c4 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 = "920" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "921" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/breeder.py b/backend/routes/breeder.py index 1afe535..3728277 100644 --- a/backend/routes/breeder.py +++ b/backend/routes/breeder.py @@ -183,6 +183,27 @@ async def admin_pending_breeders(admin=Depends(require_admin)): return [dict(r) for r in rows] +# ------------------------------------------------------------------ +# GET /api/admin/breeders — alle aktiven Züchter +# ------------------------------------------------------------------ +@router.get("/admin/breeders") +async def admin_all_breeders(admin=Depends(require_admin)): + with db() as conn: + rows = conn.execute(""" + SELECT u.id, u.name, u.email, u.created_at, u.subscription_tier, + u.breeder_status, u.last_login, + bp.zwingername, bp.rasse_text, bp.verein, bp.vdh_mitglied, + bp.stadt, bp.website, bp.verified_at, + (SELECT COUNT(*) FROM litters WHERE user_id=u.id) AS wuerfe_count, + (SELECT COUNT(*) FROM dogs WHERE user_id=u.id AND is_zucht_hund=1) AS zuchthunde_count + FROM users u + LEFT JOIN breeder_profiles bp ON bp.user_id = u.id + WHERE u.rolle = 'breeder' OR u.breeder_status = 'approved' + ORDER BY bp.verified_at DESC NULLS LAST, u.created_at DESC + """).fetchall() + return [dict(r) for r in rows] + + # ------------------------------------------------------------------ # GET /api/admin/breeder/{user_id}/documents — Dokumente eines Antrags # ------------------------------------------------------------------ diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 62b0056..f75b2cf 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -691,6 +691,7 @@ const API = (() => { updateProfile(data) { return put('/breeder/profile', data); }, adminCreateProfile() { return post('/admin/breeder/create-profile', {}); }, pendingList() { return get('/admin/breeders/pending'); }, + allList() { return get('/admin/breeders'); }, documents(userId) { return get(`/admin/breeder/${userId}/documents`); }, documentUrl(userId, docId) { return `/api/admin/breeder/${userId}/document/${docId}`; }, approve(userId) { return post(`/admin/breeder/${userId}/approve`, {}); }, diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 2860ab3..1aa2c36 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 = '920'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '921'; // ← 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 e95aa34..0f03bb4 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -1893,12 +1893,17 @@ window.Page_admin = (() => { ${UI.icon('arrows-clockwise')} Aktualisieren
    -
    Lade…
    +
    Lade…
    +
    Lade…
    `; - el.querySelector('#adm-zuchter-refresh').addEventListener('click', () => - _loadZuechterAntraege(el.querySelector('#adm-zuchter-list')) - ); - await _loadZuechterAntraege(el.querySelector('#adm-zuchter-list')); + el.querySelector('#adm-zuchter-refresh').addEventListener('click', () => { + _loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege')); + _loadZuechterListe(el.querySelector('#adm-zuchter-liste')); + }); + await Promise.all([ + _loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege')), + _loadZuechterListe(el.querySelector('#adm-zuchter-liste')), + ]); } async function _loadZuechterAntraege(el) { @@ -1912,12 +1917,20 @@ window.Page_admin = (() => { } if (!antraege.length) { - el.innerHTML = _emptyState('certificate', 'Keine offenen Anträge', 'Aktuell liegen keine Züchter-Anträge zur Prüfung vor.'); + el.innerHTML = `
    +
    Offene Anträge
    +
    + ${UI.icon('check-circle')} Keine offenen Anträge +
    +
    `; return; } el.innerHTML = ` -
    +
    +
    Offene Anträge (${antraege.length})
    +
    +
    ${antraege.map(a => `
    @@ -2072,6 +2085,74 @@ window.Page_admin = (() => { }); } + async function _loadZuechterListe(el) { + el.innerHTML = `
    Lade…
    `; + let breeders; + try { + breeders = await API.breeder.allList(); + } catch (e) { + el.innerHTML = _emptyState('warning', 'Fehler', e.message); + return; + } + + const tierBadge = t => { + if (t === 'breeder') return `Züchter-Abo`; + if (t === 'breeder_test') return `Test`; + return `Standard`; + }; + + const rows = breeders.map(b => ` + + +
    ${_esc(b.name)}
    +
    ${_esc(b.email)}
    + + ${_esc(b.zwingername || '—')} + ${_esc(b.rasse_text || '—')} + ${_esc(b.stadt || '—')} + + ${b.wuerfe_count || 0} Würfe
    + ${b.zuchthunde_count || 0} Zuchthunde + + ${tierBadge(b.subscription_tier)} + + ${b.verified_at ? new Date(b.verified_at).toLocaleDateString('de-DE') : '—'} + + + + + `).join(''); + + el.innerHTML = ` +
    +
    Alle Züchter (${breeders.length})
    +
    + + + ${['Nutzer','Zwingername','Rasse','Stadt','Aktivität','Abo','Seit',''].map(h => + `` + ).join('')} + + + ${rows || ``} + +
    ${h}
    Noch keine Züchter
    +
    +
    `; + + el.querySelectorAll('.adm-breeder-tier-btn').forEach(btn => { + btn.addEventListener('click', () => + _changeTier(btn.dataset.uid, btn.dataset.name, btn.dataset.tier) + ); + }); + } + // ------------------------------------------------------------------ async function _renderJobs(el) { el.innerHTML = ` diff --git a/backend/static/sw.js b/backend/static/sw.js index 7fe3d87..a33205f 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-v920'; +const CACHE_VERSION = 'by-v921'; 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 706e84186ea2a315943634597e2248e067392cbc Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 10:14:47 +0200 Subject: [PATCH 05/26] =?UTF-8?q?Fix:=20Admin=20Z=C3=BCchter-Liste=20?= =?UTF-8?q?=E2=80=94=20NULLS=20LAST=20durch=20CASE=20ersetzen,=20is=5Fzuch?= =?UTF-8?q?t=5Fhund=20entfernt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/breeder.py | 5 +++-- backend/static/js/pages/admin.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/routes/breeder.py b/backend/routes/breeder.py index 3728277..700fabf 100644 --- a/backend/routes/breeder.py +++ b/backend/routes/breeder.py @@ -195,11 +195,12 @@ async def admin_all_breeders(admin=Depends(require_admin)): bp.zwingername, bp.rasse_text, bp.verein, bp.vdh_mitglied, bp.stadt, bp.website, bp.verified_at, (SELECT COUNT(*) FROM litters WHERE user_id=u.id) AS wuerfe_count, - (SELECT COUNT(*) FROM dogs WHERE user_id=u.id AND is_zucht_hund=1) AS zuchthunde_count + (SELECT COUNT(*) FROM dogs WHERE user_id=u.id) AS hunde_count FROM users u LEFT JOIN breeder_profiles bp ON bp.user_id = u.id WHERE u.rolle = 'breeder' OR u.breeder_status = 'approved' - ORDER BY bp.verified_at DESC NULLS LAST, u.created_at DESC + ORDER BY CASE WHEN bp.verified_at IS NULL THEN 1 ELSE 0 END, + bp.verified_at DESC, u.created_at DESC """).fetchall() return [dict(r) for r in rows] diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 0f03bb4..00a19e8 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -2112,7 +2112,7 @@ window.Page_admin = (() => { ${_esc(b.stadt || '—')} ${b.wuerfe_count || 0} Würfe
    - ${b.zuchthunde_count || 0} Zuchthunde + ${b.hunde_count || 0} Hunde ${tierBadge(b.subscription_tier)} From 108191b339b43a759ab10c3c58dac4cf81c387a7 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 10:22:51 +0200 Subject: [PATCH 06/26] =?UTF-8?q?Fix:=20Staging=20APP=5FURL=20setzen=20?= =?UTF-8?q?=E2=80=94=20Verify-Links=20zeigten=20auf=20Production-DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.staging.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 49a7846..d9ef97e 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -13,6 +13,7 @@ services: environment: - DB_PATH=/data/banyaro.db - MEDIA_DIR=/data/media + - APP_URL=https://staging.banyaro.app - STAGING=true - KI_MODE=cloud - VAPID_PUBLIC_KEY=BMKbFAmpsqJ-eFef_4XJcYpuxPWqBNAoy9buMNnMSa6ijcPzltboHi_YccPKJrUD0isBez-vJIzAgjnLTWkzcC0 From 0a28c6895339d957d4eda6349149d6daf7992373 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 10:46:55 +0200 Subject: [PATCH 07/26] =?UTF-8?q?Fix:=20P/Z-Badges=20nur=20im=20Welten-Ein?= =?UTF-8?q?richten-Modal=20f=C3=BCr=20Admins=20(SW=20by-v922)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Normale Welt-Views (JETZT/HUND/WELT): keine P/Z-Badges auf Chips - Config-Modal: P-Badge (Pro) und Z-Badge (Züchter) nur wenn isAdmin=true - isAdmin-Variable im _openConfigModal()-Scope ergänzt --- backend/main.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/worlds.js | 8 +++++--- backend/static/sw.js | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/main.py b/backend/main.py index 9f5a2c4..80db05c 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 = "921" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "922" # 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 1aa2c36..6873247 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 = '921'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '922'; // ← 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/worlds.js b/backend/static/js/worlds.js index 0c3c15d..f4997a0 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -673,6 +673,7 @@ window.Worlds = (() => { let cfg = JSON.parse(JSON.stringify(_getConfig())); // deep copy let _drag = null; // { page, fromWorld, ghost } + const isAdmin = _state?.user?.rolle === 'admin'; const worldColors = { jetzt:'rgba(196,132,58,0.6)', hund:'rgba(196,132,58,0.8)', welt:'rgba(99,130,220,0.6)' }; const worldLabels = { jetzt:'JETZT', hund:'HUND', welt:'WELT', pool:'Nicht verwendet' }; const allAssigned = () => new Set([...cfg.jetzt, ...cfg.hund, ...cfg.welt]); @@ -776,7 +777,8 @@ window.Worlds = (() => {
    `} - ${c.pro && _isRoleBasedPro() ? `P` : ''} + ${isAdmin && c.pro ? `P` : ''} + ${isAdmin && c.role === 'breeder' ? `Z` : ''} @@ -1448,7 +1450,7 @@ window.Worlds = (() => { ` : ''}
    - ${chips.map(c => _chip(c.icon, c.label, c.page, false, c.pro && _isRoleBasedPro(), c.role === 'breeder')).join('')} + ${chips.map(c => _chip(c.icon, c.label, c.page, false, false, false)).join('')}
    diff --git a/backend/static/sw.js b/backend/static/sw.js index 8f687c7..ffc4d5c 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-v925'; +const CACHE_VERSION = 'by-v926'; 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 2a3afa06040363940a2092354d9335541f640e3e Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 11:29:59 +0200 Subject: [PATCH 12/26] =?UTF-8?q?Fix:=20KI-Notiz-Assistent=20Toggle=20nur?= =?UTF-8?q?=20f=C3=BCr=20Pro-Nutzer=20sichtbar=20(SW=20by-v927)?= 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/settings.js | 13 +++++++++++-- backend/static/sw.js | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/main.py b/backend/main.py index 40d15ce..0ede6c1 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 = "926" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "927" # 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 176c0a9..6366a57 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 = '926'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '927'; // ← 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/settings.js b/backend/static/js/pages/settings.js index 668275f..1bacd62 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -613,7 +613,15 @@ window.Page_settings = (() => { • Vollbild: Einstellungen → Display → Navigationsleiste → Wischgesten aktivieren.
    ` : ''} - + + ${(() => { + const tier = u.subscription_tier || 'standard'; + const hasPro = ['pro','breeder'].includes(tier) || + u.rolle === 'admin' || u.rolle === 'moderator' || + u.is_moderator || u.is_social_media || + tier.endsWith('_test'); + if (!hasPro) return ''; + return `
    @@ -635,7 +643,8 @@ window.Page_settings = (() => { background:#fff;transition:.2s; box-shadow:0 1px 3px rgba(0,0,0,.3)"> -
    +
    `; + })()}
    diff --git a/backend/static/sw.js b/backend/static/sw.js index ffc4d5c..aa529d6 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-v926'; +const CACHE_VERSION = 'by-v927'; 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 00457f52f98649f279f48468f582468ad13aa705 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 11:35:53 +0200 Subject: [PATCH 13/26] Fix: Datenschutz Hintergrundbild + Dog-Refresh nach Anlegen (SW by-v928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dogs.py: welcome-dashboard Foto-Queries filtern jetzt auch nach user_id - worlds.js: Background Cache-Key enthält user_id (kein Cross-User Leakage) - worlds.js: Worlds.refresh(appState) neu - dog-profile.js: Worlds.refresh() nach Hund anlegen aufrufen --- backend/main.py | 2 +- backend/routes/dogs.py | 9 +++++---- backend/static/js/app.js | 2 +- backend/static/js/pages/dog-profile.js | 3 +++ backend/static/js/worlds.js | 16 ++++++++++++++-- backend/static/sw.js | 2 +- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/backend/main.py b/backend/main.py index 0ede6c1..ad1a5ca 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 = "927" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "928" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index cc86caf..405856f 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -191,23 +191,24 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)): raise HTTPException(404, "Hund nicht gefunden.") # Hintergrundfoto: Querformat-Bilder bevorzugt, tagesweise rotierend + # user_id-Filter als zweite Sicherungsebene (dog_id-Ownership bereits oben geprüft) photos = conn.execute( """SELECT dm.url FROM diary_media dm JOIN diary d ON d.id = dm.diary_id - WHERE d.dog_id=? AND dm.media_type='image' + WHERE d.dog_id=? AND d.user_id=? AND dm.media_type='image' AND dm.img_width IS NOT NULL AND dm.img_width > dm.img_height ORDER BY d.datum DESC, d.id DESC, dm.id ASC""", - (dog_id,) + (dog_id, user["id"]) ).fetchall() # Fallback: Bilder ohne Dimensionsdaten (vor dem Backfill hochgeladen) if not photos: photos = conn.execute( """SELECT dm.url FROM diary_media dm JOIN diary d ON d.id = dm.diary_id - WHERE d.dog_id=? AND dm.media_type='image' + WHERE d.dog_id=? AND d.user_id=? AND dm.media_type='image' AND dm.img_width IS NULL ORDER BY d.datum DESC, d.id DESC, dm.id ASC""", - (dog_id,) + (dog_id, user["id"]) ).fetchall() random_photo = None if photos: diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 6366a57..ba4c022 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 = '927'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '928'; // ← 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/dog-profile.js b/backend/static/js/pages/dog-profile.js index 9399085..8e18db5 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -1388,6 +1388,9 @@ window.Page_dog_profile = (() => { // Dog Switcher in Header + Sidebar aktualisieren App.renderDogSwitcher?.(); + // Welten neu laden damit HUND-Welt den neuen Hund zeigt + window.Worlds?.refresh(_appState); + await _render(); }); }); diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js index e032542..b640775 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -924,7 +924,8 @@ window.Worlds = (() => { async function _loadDailyImage(dog) { if (!dog) return null; - const todayKey = 'bg3_' + new Date().toISOString().slice(0, 10); + const userId = _state?.user?.id || 'anon'; + const todayKey = `bg3_${userId}_` + new Date().toISOString().slice(0, 10); const cached = _wLoad(todayKey); if (cached?.data) return cached.data; try { @@ -1689,6 +1690,17 @@ window.Worlds = (() => { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } - return { init, show, hide, navigateTo, openConfig: _openConfigModal, get _visible() { return _visible; } }; + function refresh(appState) { + if (appState) _state = appState; + localStorage.removeItem('w3_dogs'); + _bgUrl = null; + if (_visible) { + if (_cur === 0) _renderJetzt(); + else if (_cur === 1) _renderHund(); + else _renderWelt(); + } + } + + return { init, show, hide, navigateTo, refresh, openConfig: _openConfigModal, get _visible() { return _visible; } }; })(); diff --git a/backend/static/sw.js b/backend/static/sw.js index aa529d6..1f31d7d 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-v927'; +const CACHE_VERSION = 'by-v928'; 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 3585958c47d0204f8e0f160502fe664d739eae3c Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 11:41:46 +0200 Subject: [PATCH 14/26] UX: Hinweis 'Welten bearbeiten' im Ausgeblendete-Funktionen-Sheet (SW by-v929) --- backend/main.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/worlds.js | 20 ++++++++++++++++++++ backend/static/sw.js | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index ad1a5ca..28aac79 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 = "928" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "929" # 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 ba4c022..22413d4 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 = '928'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '929'; // ← 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/worlds.js b/backend/static/js/worlds.js index b640775..5884d71 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -373,6 +373,22 @@ window.Worlds = (() => { ${sections || `
    Alle Funktionen sind bereits in deinen Welten sichtbar.
    `} +
    + + + + Einzelne Funktionen ausblenden oder zwischen den Welten verschieben: + + in deinem Profil. + +
    `; @@ -387,6 +403,10 @@ window.Worlds = (() => { navigateTo(btn.dataset.page); }); }); + ov.querySelector('#fab-all-goto-worlds')?.addEventListener('click', () => { + _close(); + _openConfigModal(); + }); } // ── SCHNELL-GASSI ───────────────────────────────────────────── diff --git a/backend/static/sw.js b/backend/static/sw.js index 1f31d7d..8895786 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-v928'; +const CACHE_VERSION = 'by-v929'; 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 567478a86699a0c9cb265ae085e6e823516ca9c4 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 11:47:08 +0200 Subject: [PATCH 15/26] =?UTF-8?q?Fix:=20Altes=20Hundeprofil-Foto=20wird=20?= =?UTF-8?q?beim=20=C3=9Cberschreiben=20gel=C3=B6scht=20(SW=20by-v930)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dogs.py /photo: alte foto_url vor Upload merken, nach Speichern von Disk löschen - dog-profile.js: Hintergrund-Cache nach Foto-Wechsel invalidieren --- backend/main.py | 2 +- backend/routes/dogs.py | 14 ++++++++++++-- backend/static/js/app.js | 2 +- backend/static/js/pages/dog-profile.js | 4 ++++ backend/static/sw.js | 2 +- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/main.py b/backend/main.py index 28aac79..6085b5a 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 = "929" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "930" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 405856f..7ec90f5 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -844,13 +844,14 @@ async def upload_photo( file: UploadFile = File(...), user=Depends(get_current_user) ): - # Hund gehört dem User? + # Hund gehört dem User? Altes Foto merken für späteres Löschen. with db() as conn: dog = conn.execute( - "SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"]) + "SELECT id, foto_url FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"]) ).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") + old_foto_url = dog["foto_url"] # Datei immer als JPEG speichern (HEIC/PNG/WebP → kompatibel für alle Browser) import io @@ -884,6 +885,15 @@ async def upload_photo( with db() as conn: conn.execute("UPDATE dogs SET foto_url=? WHERE id=?", (foto_url, dog_id)) + # Altes Foto von Disk löschen + if old_foto_url: + try: + old_path = safe_media_path(MEDIA_DIR, old_foto_url) + if old_path and os.path.isfile(old_path): + os.remove(old_path) + except Exception: + pass + return {"foto_url": foto_url} diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 22413d4..b950790 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 = '929'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '930'; // ← 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/dog-profile.js b/backend/static/js/pages/dog-profile.js index 8e18db5..5af569d 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -1380,6 +1380,10 @@ window.Page_dog_profile = (() => { saved.foto_url = result.foto_url; _appState.activeDog = { ...saved }; _appState.dogs = _appState.dogs.map(d => d.id === saved.id ? _appState.activeDog : d); + // Hintergrund-Cache invalidieren damit Welten das neue Foto zeigen + const userId = _appState.user?.id || 'anon'; + const todayKey = `w3_bg3_${userId}_` + new Date().toISOString().slice(0, 10); + localStorage.removeItem(todayKey); } catch { UI.toast.warning('Profil gespeichert, Foto konnte nicht hochgeladen werden.'); } diff --git a/backend/static/sw.js b/backend/static/sw.js index 8895786..c3c3957 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-v929'; +const CACHE_VERSION = 'by-v930'; 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 e7b6cb3c332b4c2f35e6949982eed2dc8c8af47c Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 11:50:43 +0200 Subject: [PATCH 16/26] =?UTF-8?q?Fix:=20Karte=20=E2=80=94=20Regenradar-=20?= =?UTF-8?q?und=20Temperatur-FABs=20nur=20f=C3=BCr=20Pro=20sichtbar=20(SW?= =?UTF-8?q?=20by-v931)?= 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/map.js | 2 ++ backend/static/sw.js | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index 6085b5a..c9200c5 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 = "930" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "931" # 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 b950790..3d4f112 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 = '930'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '931'; // ← 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/map.js b/backend/static/js/pages/map.js index 4cce79c..a79174f 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -205,8 +205,10 @@ window.Page_map = (() => {
    + ${App.hasPro(appState?.user) ? ` + ` : ''}
    diff --git a/backend/static/sw.js b/backend/static/sw.js index c3c3957..bfcf946 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-v930'; +const CACHE_VERSION = 'by-v931'; 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 781e3383bd74a237737d01a2ba031b1077444615 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 11:58:00 +0200 Subject: [PATCH 17/26] =?UTF-8?q?Fix:=20Karte=20l=C3=A4dt=20wieder=20+=20s?= =?UTF-8?q?afe=5Fmedia=5Fpath=20+=20Foto-L=C3=B6schung=20(SW=20by-v932)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - map.js: appState→_appState im Template, addEventListener mit ?. (kein Crash ohne Buttons) - media_utils.py: safe_media_path lstrip-Bug — 'dogs/' wurde zu 'ogs/' gekürzt; jetzt korrekte removeprefix-Logik → Altes Hundeprofil-Foto wird jetzt wirklich gelöscht --- backend/main.py | 2 +- backend/media_utils.py | 7 ++++++- backend/static/js/app.js | 2 +- backend/static/js/pages/map.js | 6 +++--- backend/static/sw.js | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/backend/main.py b/backend/main.py index c9200c5..83e7258 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 = "931" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "932" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/media_utils.py b/backend/media_utils.py index 4cb2e28..4fa6a82 100644 --- a/backend/media_utils.py +++ b/backend/media_utils.py @@ -56,7 +56,12 @@ def safe_media_path(media_dir: str, url: str) -> str | None: Konstruiert einen sicheren Dateipfad aus einer gespeicherten URL. Gibt None zurück wenn der Pfad außerhalb von media_dir liegt (Path-Traversal-Schutz). """ - relative = url.lstrip("/media/").lstrip("/") + if url.startswith("/media/"): + relative = url[len("/media/"):] + elif url.startswith("/"): + relative = url[1:] + else: + relative = url candidate = os.path.realpath(os.path.join(media_dir, relative)) real_base = os.path.realpath(media_dir) if not candidate.startswith(real_base + os.sep) and candidate != real_base: diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 3d4f112..ba3e8b1 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 = '931'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '932'; // ← 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/map.js b/backend/static/js/pages/map.js index a79174f..bc65d98 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -205,7 +205,7 @@ window.Page_map = (() => {
    - ${App.hasPro(appState?.user) ? ` + ${App.hasPro(_appState?.user) ? ` ` : ''} @@ -291,8 +291,8 @@ window.Page_map = (() => { }); document.getElementById('map-pin-btn').addEventListener('click', _togglePlacementMode); - document.getElementById('map-radar-btn').addEventListener('click', _toggleRadar); - document.getElementById('map-temp-btn').addEventListener('click', _toggleTemp); + document.getElementById('map-radar-btn')?.addEventListener('click', _toggleRadar); + document.getElementById('map-temp-btn')?.addEventListener('click', _toggleTemp); } // ---------------------------------------------------------- diff --git a/backend/static/sw.js b/backend/static/sw.js index bfcf946..3a62bcb 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-v931'; +const CACHE_VERSION = 'by-v932'; 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 08f1806be3221b207b71fb583b40f60ba2480d54 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 12:03:25 +0200 Subject: [PATCH 18/26] Fix: Foto-Upload im Hunde-Profil Edit-Modal (SW by-v933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: fotoFile-Referenz wurde NACH UI.modal.close() geholt — DOM-Element #dp-form-foto existiert nach Modal-Close nicht mehr → files[0] = undefined → Upload übersprungen. Fix: fotoFile VOR modal.close() sichern; File-Objekt bleibt gültig auch ohne DOM. --- backend/main.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/pages/dog-profile.js | 4 +++- backend/static/sw.js | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/main.py b/backend/main.py index 83e7258..aa1c4f9 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 = "932" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "933" # 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 ba3e8b1..44d530e 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 = '932'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '933'; // ← 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/dog-profile.js b/backend/static/js/pages/dog-profile.js index 5af569d..e8b35f2 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -1354,6 +1354,9 @@ window.Page_dog_profile = (() => { fell_typ: fd.fell_typ || null, }; + // Datei-Referenz VOR Modal-Close sichern — DOM-Element wird beim Schließen entfernt + const fotoFile = document.getElementById('dp-form-foto')?.files[0]; + let saved; if (dog) { saved = await API.dogs.update(dog.id, payload); @@ -1371,7 +1374,6 @@ window.Page_dog_profile = (() => { } // Foto hochladen wenn gewählt - const fotoFile = document.getElementById('dp-form-foto')?.files[0]; if (fotoFile) { try { const fd = new FormData(); diff --git a/backend/static/sw.js b/backend/static/sw.js index 3a62bcb..6400ec7 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-v932'; +const CACHE_VERSION = 'by-v933'; 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 aa379d8e08fab364204abcdb9e29544a28563c3c Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 12:11:59 +0200 Subject: [PATCH 19/26] =?UTF-8?q?Fix:=20Foto-Upload=20=E2=86=92=20sofortig?= =?UTF-8?q?er=20Refresh=20von=20Profil=20+=20DogSwitcher=20+=20Welten=20(S?= =?UTF-8?q?W=20by-v934)?= 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/dog-profile.js | 6 +++++- backend/static/sw.js | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/main.py b/backend/main.py index aa1c4f9..6c172d0 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 = "933" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "934" # 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 44d530e..64c0adb 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 = '933'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '934'; // ← 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/dog-profile.js b/backend/static/js/pages/dog-profile.js index e8b35f2..2d4c843 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -769,8 +769,12 @@ window.Page_dog_profile = (() => { await API.dogs.updatePhotoPosition(dog.id, 1.0, 0.0, 0.0); _appState.activeDog = { ..._appState.activeDog, foto_url: result.foto_url, foto_zoom: 1, foto_offset_x: 0, foto_offset_y: 0 }; _appState.dogs = _appState.dogs.map(d => d.id === dog.id ? _appState.activeDog : d); + // Hintergrund-Cache invalidieren + const userId2 = _appState.user?.id || 'anon'; + localStorage.removeItem(`w3_bg3_${userId2}_` + new Date().toISOString().slice(0, 10)); UI.modal.close(); - App.renderDogSwitcher(); + App.renderDogSwitcher?.(); + window.Worlds?.refresh(_appState); UI.toast.success('Foto hochgeladen.'); _renderProfile(_appState.activeDog); // Editor neu öffnen damit User positionieren kann diff --git a/backend/static/sw.js b/backend/static/sw.js index 6400ec7..a2733ff 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-v933'; +const CACHE_VERSION = 'by-v934'; 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 fbb070032a88bf44df0ea25afb2000cc27dd80a7 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 12:18:48 +0200 Subject: [PATCH 20/26] =?UTF-8?q?Fix:=20SW-Cache=20welcome-dashboard=20nac?= =?UTF-8?q?h=20Foto-Upload=20l=C3=B6schen=20(SW=20by-v935)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: /api/dogs/{id}/welcome-dashboard ist SW-gecacht. Nach Foto-Upload lieferte SW alten Response mit alter random_photo.url. Fix: API.swCacheDelete() + localStorage-Key nach Upload invalidieren. --- backend/main.py | 2 +- backend/static/js/api.js | 10 +++++++++- backend/static/js/app.js | 2 +- backend/static/js/pages/dog-profile.js | 9 +++++---- backend/static/sw.js | 2 +- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/backend/main.py b/backend/main.py index 6c172d0..6dc2e56 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 = "934" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "935" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/js/api.js b/backend/static/js/api.js index f75b2cf..8621935 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -800,9 +800,17 @@ const API = (() => { get(`/osm/pois?type=${type}&south=${south}&west=${west}&north=${north}&east=${east}&fast=true`), }; + // SW-Cache-Einträge für eine URL löschen (z.B. nach Foto-Upload) + async function swCacheDelete(path) { + try { + const c = await caches.open('ban-yaro-api-v1'); + await c.delete(new Request(path)); + } catch {} + } + // Öffentliche API return { - get, post, put, patch, del, upload, + get, post, put, patch, del, upload, swCacheDelete, auth, dogs, diary, health, tieraerzte, healthDocs, poison, places, routes, walks, events, sitting, forum, lost, knigge, weather, push, friends, chat, webcal, importData, sharing, widget, notifications, services, ratings, sittingAccess, training, notes, diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 64c0adb..fff0a5e 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 = '934'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '935'; // ← 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/dog-profile.js b/backend/static/js/pages/dog-profile.js index 2d4c843..13fbe76 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -769,9 +769,10 @@ window.Page_dog_profile = (() => { await API.dogs.updatePhotoPosition(dog.id, 1.0, 0.0, 0.0); _appState.activeDog = { ..._appState.activeDog, foto_url: result.foto_url, foto_zoom: 1, foto_offset_x: 0, foto_offset_y: 0 }; _appState.dogs = _appState.dogs.map(d => d.id === dog.id ? _appState.activeDog : d); - // Hintergrund-Cache invalidieren + // localStorage + SW-Cache invalidieren const userId2 = _appState.user?.id || 'anon'; localStorage.removeItem(`w3_bg3_${userId2}_` + new Date().toISOString().slice(0, 10)); + API.swCacheDelete(`/api/dogs/${dog.id}/welcome-dashboard`); UI.modal.close(); App.renderDogSwitcher?.(); window.Worlds?.refresh(_appState); @@ -1386,10 +1387,10 @@ window.Page_dog_profile = (() => { saved.foto_url = result.foto_url; _appState.activeDog = { ...saved }; _appState.dogs = _appState.dogs.map(d => d.id === saved.id ? _appState.activeDog : d); - // Hintergrund-Cache invalidieren damit Welten das neue Foto zeigen + // localStorage + SW-Cache invalidieren damit Welten das neue Foto zeigen const userId = _appState.user?.id || 'anon'; - const todayKey = `w3_bg3_${userId}_` + new Date().toISOString().slice(0, 10); - localStorage.removeItem(todayKey); + localStorage.removeItem(`w3_bg3_${userId}_` + new Date().toISOString().slice(0, 10)); + API.swCacheDelete(`/api/dogs/${saved.id}/welcome-dashboard`); } catch { UI.toast.warning('Profil gespeichert, Foto konnte nicht hochgeladen werden.'); } diff --git a/backend/static/sw.js b/backend/static/sw.js index a2733ff..82082f6 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-v934'; +const CACHE_VERSION = 'by-v935'; 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 2b672c89df13d96f39358aa6ec445332462cfaef Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 12:28:03 +0200 Subject: [PATCH 21/26] =?UTF-8?q?Fix:=20Welten=20refresh=20nach=20Foto-Upl?= =?UTF-8?q?oad=20=E2=80=94=20=5FrefreshPending=20Flag=20(SW=20by-v936)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit show() re-renderte nur bei User-Wechsel. Wenn refresh() während unsichtbarer Welten aufgerufen wurde, blieb das alte Bild beim Zurücknavigieren. Fix: _refreshPending=true wenn !_visible; show() rendert bei nächstem Aufruf neu. --- backend/main.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/worlds.js | 21 ++++++++++++++++----- backend/static/sw.js | 2 +- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/backend/main.py b/backend/main.py index 6dc2e56..b3a9124 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 = "935" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "936" # 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 fff0a5e..ed72597 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 = '935'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '936'; // ← 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/worlds.js b/backend/static/js/worlds.js index 5884d71..b250a29 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -5,11 +5,12 @@ window.Worlds = (() => { - let _state = null; - let _cur = 1; // 0=JETZT 1=HUND 2=WELT - let _visible = false; - let _map = null; - let _weltInited = false; + let _state = null; + let _cur = 1; // 0=JETZT 1=HUND 2=WELT + let _visible = false; + let _map = null; + let _weltInited = false; + let _refreshPending = false; // gesetzt wenn refresh() während !_visible aufgerufen wird let _lastUserId = undefined; let _dogs = []; // gecachte Hundesliste let _dogIdx = 0; // aktuell angezeigter Hund @@ -118,6 +119,14 @@ window.Worlds = (() => { if (worldIdx != null) _goTo(worldIdx, false); if (_cur === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } + // Ausstehender Refresh (z.B. nach Foto-Upload während Worlds unsichtbar) + if (_refreshPending) { + _refreshPending = false; + _renderJetzt(); + _renderHund(); + return; + } + // Nach Login/Logout: Config aus DB laden, dann rendern const currentUserId = _state?.user?.id ?? null; if (currentUserId !== _lastUserId) { @@ -1718,6 +1727,8 @@ window.Worlds = (() => { if (_cur === 0) _renderJetzt(); else if (_cur === 1) _renderHund(); else _renderWelt(); + } else { + _refreshPending = true; } } diff --git a/backend/static/sw.js b/backend/static/sw.js index 82082f6..4fa2787 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-v935'; +const CACHE_VERSION = 'by-v936'; 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 e397b9dff5ed6d298803be71b4e0596e929bc590 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 12:34:26 +0200 Subject: [PATCH 22/26] =?UTF-8?q?Fix:=20SW-Cache=20/api/dogs=20nach=20Foto?= =?UTF-8?q?-Upload=20l=C3=B6schen=20(SW=20by-v937)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _renderHund() liest Hunde aus _cachedGet('/dogs') — SW-gecacht mit altem foto_url. Fix: swCacheDelete für /api/dogs, /api/dogs/{id} + welcome-dashboard, plus w3_dogs. --- backend/main.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/pages/dog-profile.js | 6 ++++++ backend/static/sw.js | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index b3a9124..2496738 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 = "936" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "937" # 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 ed72597..cd0ced3 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 = '936'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '937'; // ← 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/dog-profile.js b/backend/static/js/pages/dog-profile.js index 13fbe76..95aa123 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -772,6 +772,9 @@ window.Page_dog_profile = (() => { // localStorage + SW-Cache invalidieren const userId2 = _appState.user?.id || 'anon'; localStorage.removeItem(`w3_bg3_${userId2}_` + new Date().toISOString().slice(0, 10)); + localStorage.removeItem('w3_dogs'); + API.swCacheDelete('/api/dogs'); + API.swCacheDelete(`/api/dogs/${dog.id}`); API.swCacheDelete(`/api/dogs/${dog.id}/welcome-dashboard`); UI.modal.close(); App.renderDogSwitcher?.(); @@ -1390,6 +1393,9 @@ window.Page_dog_profile = (() => { // localStorage + SW-Cache invalidieren damit Welten das neue Foto zeigen const userId = _appState.user?.id || 'anon'; localStorage.removeItem(`w3_bg3_${userId}_` + new Date().toISOString().slice(0, 10)); + localStorage.removeItem('w3_dogs'); + API.swCacheDelete('/api/dogs'); + API.swCacheDelete(`/api/dogs/${saved.id}`); API.swCacheDelete(`/api/dogs/${saved.id}/welcome-dashboard`); } catch { UI.toast.warning('Profil gespeichert, Foto konnte nicht hochgeladen werden.'); diff --git a/backend/static/sw.js b/backend/static/sw.js index 4fa2787..ce2b895 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-v936'; +const CACHE_VERSION = 'by-v937'; 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 3f0e09e5d2bf5a6eac90d331e06a3bd69fcf913d Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 12:42:20 +0200 Subject: [PATCH 23/26] Perf: Wiki Breed-Fotos nutzen _preview.webp + breeds/ in generate-previews (SW by-v938) --- backend/main.py | 2 +- backend/routes/admin.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/pages/wiki.js | 11 +++++++++-- backend/static/sw.js | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/main.py b/backend/main.py index 2496738..4d66590 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 = "937" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "938" # 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 19e93eb..b48720c 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -1067,7 +1067,7 @@ async def generate_media_previews(user=Depends(require_admin)): skipped = 0 errors = 0 - for subdir in ("diary", "forum"): + for subdir in ("diary", "forum", "breeds", "breeds/gallery", "breeds/submissions"): folder = os.path.join(MEDIA_DIR, subdir) if not os.path.isdir(folder): continue diff --git a/backend/static/js/app.js b/backend/static/js/app.js index cd0ced3..f7dde7c 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 = '937'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '938'; // ← 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/wiki.js b/backend/static/js/pages/wiki.js index b0c101d..86bb8d9 100644 --- a/backend/static/js/pages/wiki.js +++ b/backend/static/js/pages/wiki.js @@ -384,8 +384,13 @@ window.Page_wiki = (() => { function _breedCardHtml(r) { const fotoUrl = r.foto_url || r.user_foto || ''; + // Für lokale Bilder: _preview.webp zuerst, bei Fehler Original nachladen + const srcUrl = fotoUrl.startsWith('/media/') + ? fotoUrl.replace(/\.(jpe?g|png|gif|webp)$/i, '_preview.webp') + : fotoUrl; const photoHtml = fotoUrl - ? `${_esc(r.name)}` + ? `${_esc(r.name)}` : ''; const fallbackHtml = `
    ${_DOG_SILHOUETTE}
    `; @@ -746,7 +751,9 @@ window.Page_wiki = (() => { ${allFotos.map((f, i) => ` `).join('')}
    ` : ''} diff --git a/backend/static/sw.js b/backend/static/sw.js index ce2b895..5be62bd 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-v937'; +const CACHE_VERSION = 'by-v938'; 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 8080d7cda1b3ea55f6c90828cdb3c86c31e16e00 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 12:48:05 +0200 Subject: [PATCH 24/26] =?UTF-8?q?Fix:=20no=20such=20column=20d.user=5Fid?= =?UTF-8?q?=20=E2=80=94=20diary=20hat=20kein=20user=5Fid,=20Dog-Check=20re?= =?UTF-8?q?icht=20(SW=20by-v939)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/routes/dogs.py | 10 +++++----- backend/static/js/app.js | 2 +- backend/static/sw.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/main.py b/backend/main.py index 4d66590..bed3d6f 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 = "938" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "939" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 7ec90f5..832e777 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -191,24 +191,24 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)): raise HTTPException(404, "Hund nicht gefunden.") # Hintergrundfoto: Querformat-Bilder bevorzugt, tagesweise rotierend - # user_id-Filter als zweite Sicherungsebene (dog_id-Ownership bereits oben geprüft) + # Ownership bereits durch Dog-Check oben gesichert (dog gehört user) photos = conn.execute( """SELECT dm.url FROM diary_media dm JOIN diary d ON d.id = dm.diary_id - WHERE d.dog_id=? AND d.user_id=? AND dm.media_type='image' + WHERE d.dog_id=? AND dm.media_type='image' AND dm.img_width IS NOT NULL AND dm.img_width > dm.img_height ORDER BY d.datum DESC, d.id DESC, dm.id ASC""", - (dog_id, user["id"]) + (dog_id,) ).fetchall() # Fallback: Bilder ohne Dimensionsdaten (vor dem Backfill hochgeladen) if not photos: photos = conn.execute( """SELECT dm.url FROM diary_media dm JOIN diary d ON d.id = dm.diary_id - WHERE d.dog_id=? AND d.user_id=? AND dm.media_type='image' + WHERE d.dog_id=? AND dm.media_type='image' AND dm.img_width IS NULL ORDER BY d.datum DESC, d.id DESC, dm.id ASC""", - (dog_id, user["id"]) + (dog_id,) ).fetchall() random_photo = None if photos: diff --git a/backend/static/js/app.js b/backend/static/js/app.js index f7dde7c..cd79577 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 = '938'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '939'; // ← 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 5be62bd..cf6d85e 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-v938'; +const CACHE_VERSION = 'by-v939'; 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 947832f63d459faedcafe541ef57592bbb0f7817 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 12:54:31 +0200 Subject: [PATCH 25/26] Fix+Debug: generate-previews mit Dir-Info + no such column fix (SW by-v939) --- backend/routes/admin.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/backend/routes/admin.py b/backend/routes/admin.py index b48720c..69dceeb 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -1058,21 +1058,25 @@ async def ors_stats(user=Depends(require_mod)): @router.post("/media/generate-previews") async def generate_media_previews(user=Depends(require_admin)): - """Generiert fehlende _preview.jpg für alle Bilder in /data/media.""" - import io as _io + """Generiert fehlende _preview.webp für alle Bilder in /data/media.""" + import logging as _log from media_utils import generate_preview, _PREVIEW_EXTS + _logger = _log.getLogger(__name__) MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") generated = 0 skipped = 0 errors = 0 + dirs_info = {} for subdir in ("diary", "forum", "breeds", "breeds/gallery", "breeds/submissions"): folder = os.path.join(MEDIA_DIR, subdir) if not os.path.isdir(folder): + dirs_info[subdir] = "not found" continue - for fname in os.listdir(folder): - # Nur Original-Bilder (keine _preview, _thumb, Videos, PDFs) + files = os.listdir(folder) + dirs_info[subdir] = f"{len(files)} files" + for fname in files: low = fname.lower() if "_preview" in low or "_thumb" in low: continue @@ -1091,10 +1095,13 @@ async def generate_media_previews(user=Depends(require_admin)): generated += 1 else: skipped += 1 + _logger.warning(f"Preview None für {subdir}/{fname}") except Exception as exc: errors += 1 + _logger.error(f"Preview-Fehler {subdir}/{fname}: {exc}") - return {"generated": generated, "skipped": skipped, "errors": errors} + _logger.info(f"generate-previews: {generated} neu, {skipped} vorhanden, {errors} Fehler | dirs: {dirs_info}") + return {"generated": generated, "skipped": skipped, "errors": errors, "dirs": dirs_info} # ------------------------------------------------------------------ From 163a6ff6a5d5f8f8d6d3cfd05342b8d0fc102503 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 14 May 2026 13:01:51 +0200 Subject: [PATCH 26/26] =?UTF-8?q?Fix:=20upgrade=5Frequests=20Query=20in=20?= =?UTF-8?q?action=5Fitems=20try/except=20=E2=80=94=20sch=C3=BCtzt=20besteh?= =?UTF-8?q?ende=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,