From 3967544de7207ea7dfe6aa3bad86c1945fcdfa45 Mon Sep 17 00:00:00 2001 From: rene Date: Tue, 12 May 2026 19:24:19 +0200 Subject: [PATCH 01/36] =?UTF-8?q?Fix:=20routes.js=20SyntaxError=20?= =?UTF-8?q?=E2=80=94=20=5FstartRecInOvl=20als=20async=20deklariert=20(SW?= =?UTF-8?q?=20by-v886)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit await _recAcquireWakeLock() in nicht-async Funktion → SyntaxError → ganzes Modul crashte → Fallback "Seite in Entwicklung" auf Production. --- backend/main.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/pages/routes.js | 2 +- backend/static/sw.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/main.py b/backend/main.py index 456dc43..6bb7070 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 = "885" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "886" # 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 9c1a424..987ac82 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 = '885'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '886'; // ← 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/routes.js b/backend/static/js/pages/routes.js index 21e5d6e..32186f9 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -700,7 +700,7 @@ window.Page_routes = (() => { ovl.querySelector('#rk-rec-startbtn').addEventListener('click', _startRecInOvl); } - function _startRecInOvl() { + async function _startRecInOvl() { if (!navigator.geolocation) { UI.toast.error('GPS nicht verfügbar.'); return; } _recActive = true; _recTrack = []; _recDistKm = 0; _recStartTime = Date.now(); diff --git a/backend/static/sw.js b/backend/static/sw.js index 4bb5047..61187c5 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-v885'; +const CACHE_VERSION = 'by-v886'; 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 b8f70eb2cdc7466b1d97c6937ef4261d0f0acc9f Mon Sep 17 00:00:00 2001 From: rene Date: Tue, 12 May 2026 19:29:04 +0200 Subject: [PATCH 02/36] =?UTF-8?q?Fix:=20SW-Update=20doppelter=20Reload=20n?= =?UTF-8?q?ach=20force-update=20=E2=80=94=20controllerchange=20konsumiert?= =?UTF-8?q?=20by=5Fskip=5Fsw=5Freload=20Flag=20(SW=20by-v887)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit statechange-activated und controllerchange feuern beide beim SW-Update. statechange konsumierte den Flag und controllerchange reloadete trotzdem. Jetzt prüft statechange nur, controllerchange konsumiert. --- backend/main.py | 2 +- backend/static/index.html | 12 +++++++----- backend/static/js/app.js | 2 +- backend/static/sw.js | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/main.py b/backend/main.py index 6bb7070..e02d63d 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 = "886" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "887" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index cc215cf..557467a 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -637,11 +637,8 @@ if (!sw) return; sw.addEventListener('statechange', () => { if (sw.state === 'activated') { - // Kein zweiter Reload nach force-update - if (sessionStorage.getItem('by_skip_sw_reload')) { - sessionStorage.removeItem('by_skip_sw_reload'); - return; - } + // Flag nur prüfen, nicht konsumieren — controllerchange konsumiert ihn + if (sessionStorage.getItem('by_skip_sw_reload')) return; window.location.replace('/?_t=' + Date.now()); } }); @@ -663,7 +660,12 @@ }); // Backup: controllerchange (falls updatefound nicht feuert) + // Konsumiert auch den by_skip_sw_reload-Flag (statechange prüft ihn nur) navigator.serviceWorker.addEventListener('controllerchange', () => { + if (sessionStorage.getItem('by_skip_sw_reload')) { + sessionStorage.removeItem('by_skip_sw_reload'); + return; + } window.location.replace('/?_t=' + Date.now()); }); diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 987ac82..fc381d6 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 = '886'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '887'; // ← 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 61187c5..501b993 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-v886'; +const CACHE_VERSION = 'by-v887'; 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 7257c9e04f7ac91cb4c9d65f9480db6710aa14b9 Mon Sep 17 00:00:00 2001 From: rene Date: Tue, 12 May 2026 19:35:42 +0200 Subject: [PATCH 03/36] =?UTF-8?q?Fix:=20SW-Update=20Dauerschleife=20?= =?UTF-8?q?=E2=80=94=20controllerchange=20nicht=20auf=20Reload-Seiten=20re?= =?UTF-8?q?gistrieren=20(SW=20by-v888)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clients.claim() feuert asynchron nach Seitenstart → controllerchange auf der neu geladenen Seite → Reload → clients.claim() → loop. Fix: controllerchange nur registrieren wenn _t= NICHT im URL steht. --- backend/main.py | 2 +- backend/static/index.html | 19 +++++++++++-------- backend/static/js/app.js | 2 +- backend/static/sw.js | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/backend/main.py b/backend/main.py index e02d63d..f3955d6 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 = "887" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "888" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index 557467a..2cbf257 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -660,14 +660,17 @@ }); // Backup: controllerchange (falls updatefound nicht feuert) - // Konsumiert auch den by_skip_sw_reload-Flag (statechange prüft ihn nur) - navigator.serviceWorker.addEventListener('controllerchange', () => { - if (sessionStorage.getItem('by_skip_sw_reload')) { - sessionStorage.removeItem('by_skip_sw_reload'); - return; - } - window.location.replace('/?_t=' + Date.now()); - }); + // NICHT registrieren wenn diese Seite selbst durch einen SW-Reload entstand (_t= im URL) + // — verhindert Dauerschleife wenn clients.claim() erst nach Seitenstart feuert + if (!location.search.includes('_t=')) { + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (sessionStorage.getItem('by_skip_sw_reload')) { + sessionStorage.removeItem('by_skip_sw_reload'); + return; + } + window.location.replace('/?_t=' + Date.now()); + }); + } navigator.serviceWorker.addEventListener('message', e => { if (e.data?.type === 'QUEUE_PROCESSED') { diff --git a/backend/static/js/app.js b/backend/static/js/app.js index fc381d6..cae6c71 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 = '887'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '888'; // ← 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 501b993..06d0d08 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-v887'; +const CACHE_VERSION = 'by-v888'; 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 437901b8e226f586a133cf96ec9e09f3898444b8 Mon Sep 17 00:00:00 2001 From: rene Date: Tue, 12 May 2026 19:40:50 +0200 Subject: [PATCH 04/36] =?UTF-8?q?Fix:=20Update-Loop=20=E2=80=94=20index.ht?= =?UTF-8?q?ml=20Script-Tags=20auf=20APP=5FVER=20synchronisiert=20(SW=20by-?= =?UTF-8?q?v889)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: ?v=885 in Script-Tags → Browser cached app.js immutable für 1 Jahr. Nach force-update (löscht nur SW-Cache, nicht Browser-HTTP-Cache) lädt Browser alte app.js mit APP_VER=885 → X-App-Version: 889 Mismatch → Endlosschleife. Ab jetzt: ?v= in index.html IMMER mit APP_VER synchron halten. --- backend/main.py | 2 +- backend/static/index.html | 8 ++++---- backend/static/js/app.js | 2 +- backend/static/sw.js | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/main.py b/backend/main.py index f3955d6..5e78c9a 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 = "888" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "889" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index 2cbf257..0fd8c6e 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -583,10 +583,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index cae6c71..0c24103 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 = '888'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '889'; // ← 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 06d0d08..1941d2a 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-v888'; +const CACHE_VERSION = 'by-v889'; 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 a5b81745661633c707de5980cd4016fff529bc02 Mon Sep 17 00:00:00 2001 From: rene Date: Tue, 12 May 2026 19:41:26 +0200 Subject: [PATCH 05/36] =?UTF-8?q?Fix=20permanent:=20JS/CSS=20nie=20immutab?= =?UTF-8?q?le=20cachen=20=E2=80=94=20verhindert=20Update-Loop=20(SW=20by-v?= =?UTF-8?q?890)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browser-HTTP-Cache mit immutable lässt sich nach force-update nicht leeren. Veraltete app.js (APP_VER alt) führt zu X-App-Version Mismatch → Dauerschleife. SW übernimmt Caching sowieso → no-cache für alle JS/CSS ist sicher. --- backend/main.py | 12 +++++------- backend/static/index.html | 8 ++++---- backend/static/js/app.js | 2 +- backend/static/sw.js | 2 +- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/backend/main.py b/backend/main.py index 5e78c9a..4498809 100644 --- a/backend/main.py +++ b/backend/main.py @@ -156,17 +156,15 @@ app.add_middleware(_AppVersionMiddleware) class _CacheControlMiddleware(BaseHTTPMiddleware): """Setzt Cache-Control-Header für statische Assets. - CSS/JS: no-cache (ETag-Validierung) — iOS cached sonst ewig ohne Ablaufdatum. - Versioned Assets (?v=…): immutable — URL ändert sich bei Updates. + JS/CSS: immer no-cache — SW übernimmt Caching. Immutable wäre gefährlich, + weil Browser-HTTP-Cache nach force-update nicht geleert wird und veraltete + app.js mit falschem APP_VER eine Update-Dauerschleife verursacht. """ async def dispatch(self, request: Request, call_next): response = await call_next(request) path = request.url.path if path.startswith(("/css/", "/js/", "/icons/phosphor.svg")): - if "v=" in str(request.url.query): - response.headers["Cache-Control"] = "public, max-age=31536000, immutable" - else: - response.headers["Cache-Control"] = "no-cache, must-revalidate" + response.headers["Cache-Control"] = "no-cache, must-revalidate" return response app.add_middleware(_CacheControlMiddleware) @@ -406,7 +404,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "889" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "890" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index 0fd8c6e..39016d0 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -583,10 +583,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 0c24103..d701647 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 = '889'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '890'; // ← 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 1941d2a..250e7b2 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-v889'; +const CACHE_VERSION = 'by-v890'; 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 e8c2d5b940420274d3864cd589102117a729a7ec Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 16:26:19 +0200 Subject: [PATCH 06/36] =?UTF-8?q?Feature:=20Z=C3=BCchter-Landing-Page=20/z?= =?UTF-8?q?uechter=20=E2=80=94=20SEO,=20Vergleich,=20Features,=20CTA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 5 + backend/static/zuechter.html | 572 +++++++++++++++++++++++++++++++++++ 2 files changed, 577 insertions(+) create mode 100644 backend/static/zuechter.html diff --git a/backend/main.py b/backend/main.py index 4498809..f819647 100644 --- a/backend/main.py +++ b/backend/main.py @@ -521,6 +521,11 @@ async def info_page(): return FileResponse(f"{STATIC_DIR}/landing.html", headers={"Cache-Control": "max-age=3600"}) +@app.get("/zuechter") +async def zuechter_landing(): + return FileResponse(f"{STATIC_DIR}/zuechter.html", headers={"Cache-Control": "max-age=3600"}) + + # ------------------------------------------------------------------ # SEO: Server-gerenderete Wiki-Rassen-Übersicht /wiki/rassen # ------------------------------------------------------------------ diff --git a/backend/static/zuechter.html b/backend/static/zuechter.html new file mode 100644 index 0000000..88f5b74 --- /dev/null +++ b/backend/static/zuechter.html @@ -0,0 +1,572 @@ + + + + + + Ban Yaro für Züchter — Zucht-Management für seriöse Hundezüchter + + + + + + + + + + + + + + + + + + +
+
+ +

Für Züchter

+

Zucht-Management für
seriöse Hundezüchter

+

Stammbaum, IK-Berechnung, Gesundheitstests, Wurfverwaltung, Kaufvertrag, KI-Assistent — alles in einer App. Kostenlos. In Deutschland.

+
+ Kostenlos starten + Server in Deutschland + DSGVO-konform + VDH-kompatibel + Kein App Store nötig +
+ Jetzt als Züchter registrieren + Alle Features ansehen ↓ +
+
+ + + + +
+
+ Das kennen alle Züchter +

Züchten ist ein Vollzeitjob.
Die Verwaltung dazu auch.

+

Gesundheitstests in einer Tabelle, Wurfgewichte in einer anderen, Käufer in WhatsApp, Kaufvertrag in Word. Nichts ist verknüpft. Das kostet Zeit — die du besser mit deinen Hunden verbringst.

+
+
+
📊
+

Excel-Chaos

+

Welpengewichte in einer Datei, Gesundheitstests in einer anderen, Kosten irgendwo sonst. Nichts hängt zusammen.

+
+
+
🧮
+

IK manuell berechnen

+

Inzuchtkoeffizient vor der Verpaarung prüfen? Drei Websites, manuelles Eintippen der Ahnentafel, Kopfrechnen.

+
+
+
📱
+

Warteliste in WhatsApp

+

Wer wollte ein Mädchen? Wer hat wann angefragt? Wer steht wo auf der Liste? Alles in Chatverläufen vergraben.

+
+
+
📝
+

Kaufvertrag kopieren

+

Altes Word-Dokument öffnen, Namen ändern, vergessen zwei Felder anzupassen, ausdrucken, faxen.

+
+
+
+

ECVO vergessen

+

Der Augentest muss jährlich erneuert werden. Wann war der letzte? Irgendwo auf einem Zettel steht das Datum.

+
+
+
💸
+

Was hat der Wurf gekostet?

+

Deckgebühr, Progesterontests, Tierarzt, Futter, Material — kein Züchter kann das am Ende wirklich sagen.

+
+
+
+
+ + +
+
+ Features +

Alles was seriöse Züchter brauchen.
In einer App.

+

Von der Zuchtzulassung bis zur Welpenabgabe — Ban Yaro begleitet jeden Schritt. Kostenlos, ohne Installation, von jedem Gerät.

+ + +

Zuchtkartei & Genetik

+
+
+ +
+

Stammbaum bis 4 Generationen

+

Grafische Darstellung aller Vorfahren — Vater, Mutter, Großeltern, Urgroßeltern. Sofort sichtbar welche Linien sich kreuzen.

+
+
+
+ +
+

Inzuchtkoeffizient nach Wright

+

Automatische IK-Berechnung über 8 Generationen. Ampel-Bewertung: optimal · akzeptabel · erhöht · kritisch. Keine Website, kein Kopfrechnen.

+ Einzigartig +
+
+
+ +
+

Probeverpaarung

+

IK simulieren bevor du den Deckrüden anfragst. Welche Kombination ist genetisch am besten? Sofort sichtbar.

+ Einzigartig +
+
+
+ +
+

Gesundheitstests

+

HD, ED, OCD, Augen (ECVO), Herz, Patella, ZTP — mit Datum, Gutachter, Zertifikatsnummer, Gültigkeitsdatum und automatischen Erinnerungen.

+
+
+
+ +
+

Gentests & Risikoanalyse

+

MDR1, PRA, DM, vWD und weitere Marker. Automatische Risikoanalyse für die Kombination: klar × Träger × betroffen — welche Nachkommen sind zu erwarten?

+
+
+
+ +
+

Titel & Auszeichnungen

+

Ausstellungs-, Arbeits-, Sport-, Zucht- und Champion-Titel dokumentieren — mit Datum, Richter, Formwert. Für den öffentlichen Steckbrief des Hundes.

+
+
+
+ + +

Wurfverwaltung & Welpen

+
+
+ +
+

Wurfverwaltung

+

Jeden Wurf anlegen mit geplanten und tatsächlichem Geburtsdatum, Elterntieren, Anzahl Welpen und Verfügbarkeitsstatus.

+
+
+
+ +
+

Welpengewichte & Entwicklung

+

Tägliche Gewichtserfassung für jeden Welpen einzeln. Wachstumskurve auf einen Blick — aus dem Smartphone heraus am Wurfplatz.

+
+
+
+ +
+

Fotos für jeden Welpen

+

Fotos hochladen und je Welpe zuordnen — mit Sichtbarkeitssteuerung: öffentlich, nur auf Anfrage oder privat.

+
+
+
+ +
+

Kaufvertrag-Generator

+

Rechtssicherer Kaufvertrag automatisch befüllt — Züchter, Käufer, Welpe, Chip, Gesundheitsstatus, Impfungen. Zum Ausdrucken oder digital unterschreiben.

+ Automatisch +
+
+
+ +
+

Tierschutz-Check

+

Automatische Bewertung jeder Verpaarung auf Basis von IK und Gentests. Bei kritischen Werten: Warnung und Admin-Meldung. Verantwortungsvolle Zucht by Default.

+ Einzigartig +
+
+
+ +
+

Datenexport

+

Kompletten Zuchtbericht als HTML (druckbereit, mit Stammbaum) oder ODS-Spreadsheet (für Excel/LibreOffice) exportieren. Deine Daten gehören dir.

+
+
+
+ + +

KI-Assistent & Präsenz

+
+
+ +
+

KI-Wurfankündigung

+

Professioneller Ankündigungstext für deinen Wurf — aus den Elterndaten, Gesundheitstests und Linienbeschreibung. In Sekunden, anpassbar.

+ KI +
+
+
+ +
+

KI-Paarungsanalyse

+

KI bewertet die geplante Verpaarung: IK, Gesundheitsprofil beider Eltern, genetische Risiken — mit klarer Empfehlung: empfohlen / bedingt / nicht empfohlen.

+ KI +
+
+
+ +
+

Öffentliche Wurfbörse

+

Deine verfügbaren Würfe erscheinen automatisch in der Banyaro-Wurfbörse — für über tausend Hundebesitzer sichtbar, filterbar nach Rasse.

+
+
+
+ +
+

Öffentliches Züchter-Profil

+

Dein Zwingername, deine Rassen, deine verifizierten Zuchthunde — öffentlich einsehbar für Käufer die aktiv nach deiner Linie suchen.

+
+
+
+ +
+

KI-Jahresbericht

+

Automatischer Jahresrückblick für deinen Zuchtstätte: Würfe, Gesundheitstrends, Auszeichnungen — als professionelles Dokument.

+ KI +
+
+
+ +
+

Genetik-Erklärung für Käufer

+

Komplizierte Gentestergebnisse in verständlichem Deutsch für Käufer erklärt — automatisch, auf Knopfdruck.

+ KI +
+
+
+
+
+ + +
+
+ Marktvergleich +

Was andere können.
Was nur Ban Yaro kann.

+

Der deutsche Markt für Züchter-Software ist dünn. Hier ein ehrlicher Vergleich mit den relevantesten Alternativen.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureBan YaroWebreedPuppy CenterHundescout
Stammbaum grafisch
IK-Berechnung (Wright)✓ 8 Gen.
Probeverpaarung mit IK
Gesundheitstests (HD, ED, …)✓ 7 Typen
Gentests + Risikoanalyse
Tierschutz-Check automatisch
Wurfverwaltung + Welpen
Kaufvertrag-Generator
KI-Assistent (Texte, Analyse)✓ 5 Features
Käufer nutzen dieselbe App
Native Mobile App✓ iOS + AndroidWebWindows only
Deutschsprachig
Server in DeutschlandFrankreich
Kostenlos starten✓ (mit Werbung)✓ (1 Hündin)ab 99 €
+
+
+
+ + +
+
+ Der entscheidende Unterschied +

Züchter und Käufer.
In einer App.

+

Kein anderes Züchter-Tool kann das: Webreed, Hundescout, Breedera — sie alle enden bei der Welpenabgabe. Was danach kommt, passiert woanders.

+ +
+

Der Welpe geht mit.

+

+ Ein Käufer der Ban Yaro nutzt kann deinen Wurf direkt in der App entdecken, anfragen und den Welpen reservieren. + Bei der Abgabe überträgst du das komplette Profil — Gewichtsverlauf, alle Impfungen, Gesundheitsstatus, Stammbaum — + direkt in die App des Käufers. Kein PDF, kein Papierstapel, kein vergessener Impfausweis. + Der Käufer hat alles. Und bleibt mit dir verbunden. +

+
+ +
+
+ +
+

Community die dich findet

+

Über tausend aktive Hundehalter nutzen Ban Yaro — die deinen Wurf in der Wurfbörse sehen, ohne dass du Werbung schalten musst.

+
+
+
+ +
+

Nachsorge ohne Aufwand

+

Käufer können dich direkt in der App kontaktieren. Wie entwickelt sich der Welpe? Hat er Fragen? Du bist erreichbar — ohne WhatsApp-Chaos.

+
+
+
+ +
+

Verifizierter Züchter-Badge

+

Nach Prüfung durch unser Team bekommst du den Verifiziert-Badge. Das schafft Vertrauen — und unterscheidet dich klar von Vermehrern.

+
+
+
+
+
+ + +
+
+ So einfach geht's +

In 10 Minuten startklar.

+

Kein App Store, keine Installation. Ban Yaro läuft im Browser — auf dem Smartphone genauso wie am Laptop.

+
+
+

Registrieren

+

Konto anlegen und Züchter-Antrag stellen — dauert 2 Minuten. E-Mail + Zwingername reicht für den Anfang.

+
+
+

Verifiziert werden

+

Wir prüfen deinen Antrag und schalten deinen Züchter-Bereich frei — meist innerhalb von 24 Stunden.

+
+
+

Zuchthunde anlegen

+

Deine Zuchthündinnen und -rüden mit Gesundheitstests, Gentests und Titeln erfassen. Der Stammbaum baut sich automatisch auf.

+
+
+

Ersten Wurf eintragen

+

Wurf anlegen, Welpen erfassen, Gewichte tracken, Fotos hochladen — und direkt in der Wurfbörse veröffentlichen.

+
+
+
+
+ + +
+
+ Vertrauen & Datenschutz +

Deine Zuchtdaten. Dein Eigentum.

+

Genealogiedaten, Gesundheitszertifikate, Kaufverträge — das sind sensible Informationen. Sie gehören dir, nicht uns.

+
+
+ +
+

Server in Deutschland

+

Alle Daten liegen auf unserem Server in Deutschland. Kein US-Anbieter, kein Drittland-Transfer. DSGVO-konform by Default.

+
+
+
+ +
+

Vollständiger Datenexport

+

Alle deine Daten jederzeit als HTML oder ODS exportieren. Du bist nie eingesperrt — deine Zuchtdaten gehören dir.

+
+
+
+ +
+

Sichtbarkeit selbst steuern

+

Gesundheitstests, Fotos, Welpen-Status — für jeden Eintrag bestimmst du: öffentlich, nur auf Anfrage, oder privat.

+
+
+
+ +
+

Echter Ansprechpartner

+

hallo@banyaro.app — kein Ticket-System, kein Bot. René antwortet persönlich.

+
+
+
+
+
+ + +
+
+

Jetzt kostenlos starten.

+

Züchter-Antrag stellen, Zuchthunde anlegen, ersten Wurf veröffentlichen — alles kostenlos, kein App Store, keine Kreditkarte.

+ Als Züchter registrieren +

Fragen? hallo@banyaro.app

+
+
+ + + + + From 67e68bbe2ddb6b4e6b406aaf1cf468aca87d6656 Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 16:45:46 +0200 Subject: [PATCH 07/36] =?UTF-8?q?Feature:=20Warteliste=20pro=20Wurf=20?= =?UTF-8?q?=E2=80=94=20CRUD,=20Status-Flow,=20Formular=20(SW=20by-v891)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/database.py | 22 ++++ backend/main.py | 2 +- backend/routes/litters.py | 95 ++++++++++++++ backend/static/index.html | 8 +- backend/static/js/api.js | 5 + backend/static/js/app.js | 2 +- backend/static/js/pages/litters.js | 195 +++++++++++++++++++++++++++++ backend/static/sw.js | 2 +- 8 files changed, 324 insertions(+), 7 deletions(-) diff --git a/backend/database.py b/backend/database.py index 3d9757f..af5298b 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2265,6 +2265,28 @@ def _migrate(conn_factory): except Exception as e: logger.warning(f"Migration behavior_log: {e}") + try: + conn.execute(""" + CREATE TABLE IF NOT EXISTS litter_waitlist ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + litter_id INTEGER NOT NULL REFERENCES litters(id) ON DELETE CASCADE, + name TEXT NOT NULL, + email TEXT, + telefon TEXT, + nachricht TEXT, + wunsch_geschlecht TEXT DEFAULT 'egal', + wunsch_farbe TEXT, + prioritaet INTEGER DEFAULT 0, + status TEXT DEFAULT 'anfrage', + notiz TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_waitlist_litter ON litter_waitlist(litter_id, prioritaet)") + logger.info("Migration: litter_waitlist bereit.") + except Exception as e: + logger.warning(f"Migration litter_waitlist: {e}") + # route_dogs: bestehende Routen allen Hunden des Users zuweisen try: existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0] diff --git a/backend/main.py b/backend/main.py index f819647..4485c8d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -404,7 +404,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "890" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "891" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/litters.py b/backend/routes/litters.py index ddc810c..c9dde95 100644 --- a/backend/routes/litters.py +++ b/backend/routes/litters.py @@ -650,3 +650,98 @@ async def generate_contract( """ return HTMLResponse(content=html) + + +# ------------------------------------------------------------------ +# Warteliste +# ------------------------------------------------------------------ +class WaitlistEntry(BaseModel): + name: str + email: Optional[str] = None + telefon: Optional[str] = None + nachricht: Optional[str] = None + wunsch_geschlecht: str = "egal" + wunsch_farbe: Optional[str] = None + prioritaet: int = 0 + status: str = "anfrage" + notiz: Optional[str] = None + + +class WaitlistUpdate(BaseModel): + name: Optional[str] = None + email: Optional[str] = None + telefon: Optional[str] = None + nachricht: Optional[str] = None + wunsch_geschlecht: Optional[str] = None + wunsch_farbe: Optional[str] = None + prioritaet: Optional[int] = None + status: Optional[str] = None + notiz: Optional[str] = None + + +@router.get("/litters/{litter_id}/waitlist") +async def get_waitlist(litter_id: int, user=Depends(_require_breeder)): + with db() as conn: + _check_litter_owner(litter_id, user, conn) + rows = conn.execute( + "SELECT * FROM litter_waitlist WHERE litter_id=? ORDER BY prioritaet, created_at", + (litter_id,) + ).fetchall() + return [dict(r) for r in rows] + + +@router.post("/litters/{litter_id}/waitlist", status_code=201) +async def add_waitlist_entry(litter_id: int, body: WaitlistEntry, user=Depends(_require_breeder)): + with db() as conn: + _check_litter_owner(litter_id, user, conn) + cur = conn.execute( + """INSERT INTO litter_waitlist + (litter_id, name, email, telefon, nachricht, wunsch_geschlecht, wunsch_farbe, + prioritaet, status, notiz) + VALUES (?,?,?,?,?,?,?,?,?,?)""", + (litter_id, body.name, body.email, body.telefon, body.nachricht, + body.wunsch_geschlecht, body.wunsch_farbe, body.prioritaet, body.status, body.notiz) + ) + row = conn.execute("SELECT * FROM litter_waitlist WHERE id=?", (cur.lastrowid,)).fetchone() + return dict(row) + + +@router.put("/litters/waitlist/{entry_id}") +async def update_waitlist_entry(entry_id: int, body: WaitlistUpdate, user=Depends(_require_breeder)): + with db() as conn: + entry = conn.execute( + """SELECT w.*, bp.user_id AS owner_user_id FROM litter_waitlist w + JOIN litters l ON l.id = w.litter_id + JOIN breeder_profiles bp ON bp.id = l.breeder_id + WHERE w.id=?""", + (entry_id,) + ).fetchone() + if not entry: + raise HTTPException(404, "Eintrag nicht gefunden.") + if user["rolle"] != "admin" and entry["owner_user_id"] != user["id"]: + raise HTTPException(403, "Kein Zugriff.") + + fields = {k: v for k, v in body.model_dump().items() if v is not None} + if not fields: + return dict(entry) + sets = ", ".join(f"{k}=?" for k in fields) + conn.execute(f"UPDATE litter_waitlist SET {sets} WHERE id=?", (*fields.values(), entry_id)) + row = conn.execute("SELECT * FROM litter_waitlist WHERE id=?", (entry_id,)).fetchone() + return dict(row) + + +@router.delete("/litters/waitlist/{entry_id}", status_code=204) +async def delete_waitlist_entry(entry_id: int, user=Depends(_require_breeder)): + with db() as conn: + entry = conn.execute( + """SELECT w.id, bp.user_id AS owner_user_id FROM litter_waitlist w + JOIN litters l ON l.id = w.litter_id + JOIN breeder_profiles bp ON bp.id = l.breeder_id + WHERE w.id=?""", + (entry_id,) + ).fetchone() + if not entry: + raise HTTPException(404, "Eintrag nicht gefunden.") + if user["rolle"] != "admin" and entry["owner_user_id"] != user["id"]: + raise HTTPException(403, "Kein Zugriff.") + conn.execute("DELETE FROM litter_waitlist WHERE id=?", (entry_id,)) diff --git a/backend/static/index.html b/backend/static/index.html index 39016d0..49a2065 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -583,10 +583,10 @@ - - - - + + + + diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 7a341a1..bb24d55 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -709,6 +709,11 @@ const API = (() => { addPuppy(id, data) { return post(`/litters/${id}/puppies`, data); }, updatePuppy(id, data) { return put(`/litters/puppies/${id}`, data); }, addWeight(id, data) { return post(`/litters/puppies/${id}/weight`, data); }, + // Warteliste + waitlist(id) { return get(`/litters/${id}/waitlist`); }, + addWaitlist(id, data) { return post(`/litters/${id}/waitlist`, data); }, + updateWaitlist(entryId, data) { return put(`/litters/waitlist/${entryId}`, data); }, + removeWaitlist(entryId) { return del(`/litters/waitlist/${entryId}`); }, // Öffentlich public(params) { return get('/litters?' + new URLSearchParams(params || {}).toString()); }, detail(id) { return get(`/litters/${id}`); }, diff --git a/backend/static/js/app.js b/backend/static/js/app.js index d701647..b84cbc0 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 = '890'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '891'; // ← 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/litters.js b/backend/static/js/pages/litters.js index 88a89f3..9675f67 100644 --- a/backend/static/js/pages/litters.js +++ b/backend/static/js/pages/litters.js @@ -206,6 +206,14 @@ window.Page_litters = (() => { }); }); + el.querySelectorAll('.litters-waitlist-btn').forEach(btn => { + btn.addEventListener('click', () => _toggleWaitlist(parseInt(btn.dataset.id))); + }); + + el.querySelectorAll('.litters-add-waitlist-btn').forEach(btn => { + btn.addEventListener('click', () => _showWaitlistForm(parseInt(btn.dataset.id), null)); + }); + // Aufgeklappten Wurf wiederherstellen if (_openId) _togglePuppies(_openId, true); } @@ -248,6 +256,10 @@ window.Page_litters = (() => { title="Welpen anzeigen"> ${UI.icon('caret-down')} Welpen + + `; } @@ -561,6 +582,180 @@ window.Page_litters = (() => { } } + // ---------------------------------------------------------- + // Warteliste + // ---------------------------------------------------------- + const _WL_STATUS = { + anfrage: { label: 'Anfrage', color: '#6b7280' }, + vorgemerkt: { label: 'Vorgemerkt', color: '#f59e0b' }, + bestaetigt: { label: 'Bestätigt', color: '#3b82f6' }, + abgegeben: { label: 'Abgegeben', color: '#16a34a' }, + abgesagt: { label: 'Abgesagt', color: '#dc2626' }, + }; + + function _wlStatusBadge(status) { + const s = _WL_STATUS[status] || _WL_STATUS.anfrage; + return `${s.label}`; + } + + async function _toggleWaitlist(litterId) { + const wrap = document.getElementById(`waitlist-wrap-${litterId}`); + if (!wrap) return; + const isOpen = wrap.style.display !== 'none'; + if (isOpen) { wrap.style.display = 'none'; return; } + wrap.style.display = ''; + await _loadWaitlist(litterId); + } + + async function _loadWaitlist(litterId) { + const inner = document.getElementById(`waitlist-inner-${litterId}`); + if (!inner) return; + try { + const entries = await API.litters.waitlist(litterId); + _renderWaitlist(inner, litterId, entries); + } catch (err) { + inner.innerHTML = `

${_esc(err.message || 'Fehler.')}

`; + } + } + + function _renderWaitlist(container, litterId, entries) { + if (!entries.length) { + container.innerHTML = `

Noch keine Interessenten eingetragen.

`; + return; + } + container.innerHTML = ` +
+ ${entries.map((e, i) => ` +
+
${i + 1}
+
+
+ ${_esc(e.name)} + ${_wlStatusBadge(e.status)} + ${e.wunsch_geschlecht && e.wunsch_geschlecht !== 'egal' ? `${e.wunsch_geschlecht === 'maennlich' ? '♂ Rüde' : '♀ Hündin'}` : ''} + ${e.wunsch_farbe ? `${_esc(e.wunsch_farbe)}` : ''} +
+
+ ${e.email ? `${UI.icon('envelope')} ${_esc(e.email)}` : ''} + ${e.telefon ? `${UI.icon('phone')} ${_esc(e.telefon)}` : ''} + ${UI.icon('calendar-dots')} ${e.created_at ? e.created_at.slice(0, 10) : '—'} +
+ ${e.nachricht ? `
"${_esc(e.nachricht)}"
` : ''} + ${e.notiz ? `
${UI.icon('note-pencil')} ${_esc(e.notiz)}
` : ''} +
+
+ + +
+
`).join('')} +
`; + + container.querySelectorAll('.wl-edit-btn').forEach(btn => { + btn.addEventListener('click', () => { + const entry = entries.find(e => e.id === parseInt(btn.dataset.entryId)); + if (entry) _showWaitlistForm(litterId, entry); + }); + }); + + container.querySelectorAll('.wl-delete-btn').forEach(btn => { + btn.addEventListener('click', async () => { + if (!window.confirm('Interessenten aus der Warteliste entfernen?')) return; + try { + await API.litters.removeWaitlist(parseInt(btn.dataset.entryId)); + await _loadWaitlist(litterId); + } catch (err) { UI.toast.error(err.message || 'Fehler.'); } + }); + }); + } + + function _showWaitlistForm(litterId, entry) { + const isEdit = !!entry; + const v = entry || {}; + UI.modal.open({ + title: isEdit ? 'Interessent bearbeiten' : 'Interessent eintragen', + body: ` +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
`, + footer: ` + + `, + }); + + document.getElementById('wl-form').addEventListener('submit', async e => { + e.preventDefault(); + const fd = new FormData(e.target); + const data = { + name: fd.get('name')?.trim(), + email: fd.get('email')?.trim() || null, + telefon: fd.get('telefon')?.trim() || null, + nachricht: fd.get('nachricht')?.trim() || null, + wunsch_geschlecht: fd.get('wunsch_geschlecht'), + wunsch_farbe: fd.get('wunsch_farbe')?.trim() || null, + prioritaet: parseInt(fd.get('prioritaet')) || 0, + status: fd.get('status'), + notiz: fd.get('notiz')?.trim() || null, + }; + try { + if (isEdit) { + await API.litters.updateWaitlist(entry.id, data); + } else { + await API.litters.addWaitlist(litterId, data); + } + UI.modal.close(); + await _loadWaitlist(litterId); + UI.toast.success(isEdit ? 'Gespeichert.' : 'Interessent eingetragen.'); + } catch (err) { UI.toast.error(err.message || 'Fehler.'); } + }); + } + // ---------------------------------------------------------- // Wurf-Formular (neu / bearbeiten) // ---------------------------------------------------------- diff --git a/backend/static/sw.js b/backend/static/sw.js index 250e7b2..4b9693e 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-v890'; +const CACHE_VERSION = 'by-v891'; 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 99842909e481989173629a62556bcc56c4e18a5e Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 16:52:39 +0200 Subject: [PATCH 08/36] =?UTF-8?q?Fix:=20Warteliste=20=E2=80=94=20besserer?= =?UTF-8?q?=20Empty-State,=20Badge-Count=20am=20Button,=20Status-=C3=9Cber?= =?UTF-8?q?sicht=20(SW=20by-v892)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/static/index.html | 8 ++++---- backend/static/js/app.js | 2 +- backend/static/js/pages/litters.js | 33 ++++++++++++++++++++++++++++-- backend/static/sw.js | 2 +- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/backend/main.py b/backend/main.py index 4485c8d..6d96fc8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -404,7 +404,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "891" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "892" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index 49a2065..5bbd100 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -583,10 +583,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index b84cbc0..def8158 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 = '891'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '892'; // ← 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/litters.js b/backend/static/js/pages/litters.js index 9675f67..d1f42d7 100644 --- a/backend/static/js/pages/litters.js +++ b/backend/static/js/pages/litters.js @@ -613,17 +613,46 @@ window.Page_litters = (() => { try { const entries = await API.litters.waitlist(litterId); _renderWaitlist(inner, litterId, entries); + // Badge am Button aktualisieren + const btn = document.querySelector(`.litters-waitlist-btn[data-id="${litterId}"]`); + if (btn) { + const active = entries.filter(e => e.status !== 'abgesagt').length; + btn.innerHTML = `${UI.icon('list-bullets')} Warteliste${active ? ` ${active}` : ''}`; + } } catch (err) { inner.innerHTML = `

${_esc(err.message || 'Fehler.')}

`; } } function _renderWaitlist(container, litterId, entries) { + const active = entries.filter(e => e.status !== 'abgesagt'); + const statusCounts = {}; + entries.forEach(e => { statusCounts[e.status] = (statusCounts[e.status] || 0) + 1; }); + + const summaryPills = Object.entries(statusCounts).map(([s, n]) => { + const cfg = _WL_STATUS[s] || _WL_STATUS.anfrage; + return `${cfg.label}: ${n}`; + }).join(''); + + const header = ` +
+
+ ${entries.length} Interessent${entries.length !== 1 ? 'en' : ''} + ${summaryPills} +
+
`; + if (!entries.length) { - container.innerHTML = `

Noch keine Interessenten eingetragen.

`; + container.innerHTML = ` +
+
${UI.icon('users')}
+

Noch keine Interessenten

+

Trage Anfragen ein — mit Wunsch-Geschlecht, Kontaktdaten und Status.

+
`; return; } - container.innerHTML = ` + + container.innerHTML = header + `
${entries.map((e, i) => `
diff --git a/backend/static/sw.js b/backend/static/sw.js index 4b9693e..c73e179 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-v891'; +const CACHE_VERSION = 'by-v892'; 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 5a639d47a994b09af9444363e0030c8ba838759c Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 16:59:03 +0200 Subject: [PATCH 09/36] =?UTF-8?q?UX:=20W=C3=BCrfe-Seite=20=E2=80=94=20Card?= =?UTF-8?q?-Design,=20Stats-Header,=20Countdown,=20bessere=20Aktionen=20(S?= =?UTF-8?q?W=20by-v893)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/static/index.html | 8 +- backend/static/js/app.js | 2 +- backend/static/js/pages/litters.js | 175 +++++++++++++++++++---------- backend/static/sw.js | 2 +- 5 files changed, 121 insertions(+), 68 deletions(-) diff --git a/backend/main.py b/backend/main.py index 6d96fc8..5b1f761 100644 --- a/backend/main.py +++ b/backend/main.py @@ -404,7 +404,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "892" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "893" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index 5bbd100..cc05416 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -583,10 +583,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index def8158..2c1b582 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 = '892'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '893'; // ← 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/litters.js b/backend/static/js/pages/litters.js index d1f42d7..12ced25 100644 --- a/backend/static/js/pages/litters.js +++ b/backend/static/js/pages/litters.js @@ -100,6 +100,7 @@ window.Page_litters = (() => { ${UI.icon('plus')} Neuer Wurf
+

Lädt…

@@ -132,6 +133,33 @@ window.Page_litters = (() => { // ---------------------------------------------------------- // Würfe-Liste rendern // ---------------------------------------------------------- + function _renderStats() { + const bar = document.getElementById('litters-stats'); + if (!bar || !_litters.length) return; + const total = _litters.length; + const aktiv = _litters.filter(l => l.status === 'verfuegbar' || l.status === 'geboren').length; + const geplant = _litters.filter(l => l.status === 'geplant').length; + const welpen = _litters.reduce((s, l) => s + (l.welpen_gesamt || 0), 0); + const verfuegb = _litters.reduce((s, l) => s + (l.welpen_verfuegbar || 0), 0); + const statItems = [ + { icon: 'list-bullets', label: 'Würfe gesamt', val: total }, + { icon: 'baby', label: 'Aktiv', val: aktiv, color: 'var(--c-success)' }, + { icon: 'calendar-dots',label: 'Geplant', val: geplant }, + { icon: 'dog', label: 'Welpen', val: welpen }, + { icon: 'tag', label: 'Verfügbar', val: verfuegb, color: verfuegb > 0 ? 'var(--c-primary)' : undefined }, + ]; + bar.style.display = 'flex'; + bar.innerHTML = statItems.map(s => ` +
+ ${UI.icon(s.icon)} +
+
${s.val}
+
${s.label}
+
+
`).join(''); + } + function _renderList() { const el = document.getElementById('litters-list'); if (!el) return; @@ -149,6 +177,7 @@ window.Page_litters = (() => { return; } + _renderStats(); el.innerHTML = _litters.map(l => _litterCardHTML(l)).join(''); // Events @@ -218,74 +247,96 @@ window.Page_litters = (() => { if (_openId) _togglePuppies(_openId, true); } + function _daysUntil(dateStr) { + if (!dateStr) return null; + const diff = Math.ceil((new Date(dateStr) - new Date()) / 86400000); + return diff; + } + function _litterCardHTML(l) { - const verfuegbar = l.welpen_verfuegbar != null ? l.welpen_verfuegbar : '?'; - const gesamt = l.welpen_gesamt != null ? l.welpen_gesamt : '?'; - const datumLabel = l.geburt_datum - ? `Geburt: ${_fmtDate(l.geburt_datum)}` - : l.erwartetes_datum - ? `Erwartet: ${_fmtDate(l.erwartetes_datum)}` - : '—'; + const verfuegbar = l.welpen_verfuegbar != null ? l.welpen_verfuegbar : '?'; + const gesamt = l.welpen_gesamt != null ? l.welpen_gesamt : '?'; + const elternLabel = [l.vater_name, l.mutter_name].filter(Boolean).map(n => _esc(n)).join(' × ') || '—'; - const elternLabel = [l.vater_name, l.mutter_name] - .filter(Boolean) - .map(n => _esc(n)) - .join(' × ') || '—'; + // Datum + Countdown + let datumChip = ''; + const refDate = l.geburt_datum || l.erwartetes_datum; + if (refDate) { + const days = _daysUntil(refDate); + const label = l.geburt_datum ? `Geburt ${_fmtDate(l.geburt_datum)}` : `Erwartet ${_fmtDate(l.erwartetes_datum)}`; + let countdownHtml = ''; + if (days !== null && !l.geburt_datum) { + const c = days < 0 ? `überfällig` + : days === 0 ? `heute!` + : days <= 7 ? `${days}d` + : `${days}d`; + countdownHtml = ` · ${c}`; + } + datumChip = `${UI.icon('calendar-dots')} ${label}${countdownHtml}`; + } - const sichtbarLabel = l.sichtbar - ? `${UI.icon('eye')} Öffentlich` - : `${UI.icon('eye-slash')} Nicht öffentlich`; + const sichtbarChip = l.sichtbar + ? `${UI.icon('eye')} Öffentlich` + : `${UI.icon('eye-slash')} Nicht öffentlich`; + + const welpenChip = `${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar`; + + const preisChip = l.preis_spanne + ? `${UI.icon('currency-eur')} ${_esc(l.preis_spanne)}` + : ''; return ` -
-
-
-
- ${elternLabel} - ${_statusBadge(l.status)} +
+ + +
+
+
+
+ ${elternLabel} + ${_statusBadge(l.status)} + ${sichtbarChip} +
+
+ ${datumChip} + ${welpenChip} + ${preisChip} +
-
- ${UI.icon('calendar-dots')} ${_esc(datumLabel)}  ·  - ${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar -  ·  ${sichtbarLabel} +
+ + + + + ${_appState.user?.ki_zucht_wurfankuendigung !== 0 ? ` + ` : ''} + +
- ${l.preis_spanne ? `
${UI.icon('currency-eur')} ${_esc(l.preis_spanne)}
` : ''} -
-
- - - - - ${_appState.user?.ki_zucht_wurfankuendigung !== 0 ? ` - ` : ''} - -
+ ${l.beschreibung ? `

${_esc(l.beschreibung)}

` : ''}
- ${l.beschreibung ? `
${_esc(l.beschreibung)}
` : ''} - -
+
diff --git a/backend/static/sw.js b/backend/static/sw.js index 7e72cb5..f4e2229 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-v904'; +const CACHE_VERSION = 'by-v905'; 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 f35a0efbaf955e867ce5defcd06c4d8297073758 Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 19:27:01 +0200 Subject: [PATCH 22/36] =?UTF-8?q?UX:=20Zuchtkartei=20Toolbar=20flex-wrap?= =?UTF-8?q?=20=E2=80=94=20kein=20horizontaler=20Scroll=20mehr=20(SW=20by-v?= =?UTF-8?q?906)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/static/index.html | 8 ++++---- backend/static/js/app.js | 2 +- backend/static/js/pages/zuchthunde.js | 2 +- backend/static/sw.js | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/main.py b/backend/main.py index 7b73223..d27dcc8 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 = "905" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "906" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index 69ae32d..575996b 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -591,10 +591,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 4e9652d..121ed79 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 = '905'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '906'; // ← 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/zuchthunde.js b/backend/static/js/pages/zuchthunde.js index a9eefc6..e0ecbfa 100644 --- a/backend/static/js/pages/zuchthunde.js +++ b/backend/static/js/pages/zuchthunde.js @@ -101,7 +101,7 @@ window.Page_zuchthunde = (() => { function _render() { _container.innerHTML = `
-
+

${UI.icon('dog')} Zuchtkartei

diff --git a/backend/static/sw.js b/backend/static/sw.js index f4e2229..848e234 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-v905'; +const CACHE_VERSION = 'by-v906'; 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 cf1ed0fdbcbbf0edd0ce829bf93fb67366b7e6ed Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 19:31:01 +0200 Subject: [PATCH 23/36] =?UTF-8?q?Fix:=20Z=C3=BCchter-Seiten=20page-contain?= =?UTF-8?q?er-wide=20(1040px)=20statt=20680px=20=E2=80=94=20Wurfb=C3=B6rse?= =?UTF-8?q?,=20Zuchtkartei,=20W=C3=BCrfe,=20L=C3=A4ufigkeit=20(SW=20by-v90?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/static/index.html | 18 +++++++++--------- backend/static/js/app.js | 2 +- backend/static/sw.js | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/main.py b/backend/main.py index d27dcc8..c3c8728 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 = "906" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "907" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index 575996b..a8f5bd5 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -430,21 +430,21 @@
-
+
-
+
-
+
-
+
-
+
@@ -591,10 +591,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 121ed79..a8c359b 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 = '906'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '907'; // ← 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 848e234..0ececf1 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-v906'; +const CACHE_VERSION = 'by-v907'; 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 0399282e4c010dd15a031e7da69fd33d48999d2f Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 19:35:05 +0200 Subject: [PATCH 24/36] =?UTF-8?q?Fix:=20Wurfb=C3=B6rse=20auto-fill=20Grid?= =?UTF-8?q?=20=E2=80=94=201=20Karte=20f=C3=BCllt=20volle=20Breite,=20keine?= =?UTF-8?q?=20fixen=20Spalten=20mehr=20(SW=20by-v907)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/static/css/components.css | 10 +--------- backend/static/index.html | 6 +++--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 0a6c754..976aef5 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -6977,18 +6977,10 @@ svg.empty-state-icon { .wb-cards { display: grid; - grid-template-columns: 1fr; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--space-4); } -@media (min-width: 768px) { - .wb-cards { grid-template-columns: repeat(2, 1fr); } -} - -@media (min-width: 1200px) { - .wb-cards { grid-template-columns: repeat(3, 1fr); } -} - .wb-card { background: var(--c-surface); border: 1px solid var(--c-border-light); diff --git a/backend/static/index.html b/backend/static/index.html index a8f5bd5..3d73a3b 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -101,9 +101,9 @@ - - - + + + From 7fb4177dbd6a895ad5c68625316437f9975bb09b Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 19:40:21 +0200 Subject: [PATCH 25/36] =?UTF-8?q?Fix:=20Breeder-FAB=20an=20document.body?= =?UTF-8?q?=20=E2=80=94=20verschwindet=20zuverl=C3=A4ssig=20via=20destroy(?= =?UTF-8?q?)=20(SW=20by-v908)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/static/index.html | 8 ++++---- backend/static/js/app.js | 2 +- backend/static/js/pages/breeder.js | 30 +++++++++++++++--------------- backend/static/sw.js | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/backend/main.py b/backend/main.py index c3c8728..e37b793 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 = "907" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "908" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index 3d73a3b..cc1f098 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -591,10 +591,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index a8c359b..e3e08be 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 = '907'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '908'; // ← 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/breeder.js b/backend/static/js/pages/breeder.js index 705d484..58edb3d 100644 --- a/backend/static/js/pages/breeder.js +++ b/backend/static/js/pages/breeder.js @@ -28,22 +28,22 @@ window.Page_breeder = (() => { container.innerHTML = `
${UI.skeleton(5)} -
- `; +
`; - document.getElementById('breeder-back-fab') - ?.addEventListener('click', () => App.navigate('wurfboerse')); + // FAB an document.body hängen damit position:fixed zuverlässig funktioniert + // und destroy() der einzige Lifecycle-Kontrollpunkt bleibt + const fab = document.createElement('button'); + fab.id = 'breeder-back-fab'; + fab.setAttribute('aria-label', 'Zurück zur Wurfbörse'); + fab.style.cssText = 'position:fixed;bottom:calc(var(--safe-bottom,0px) + 20px);right:20px;' + + 'width:54px;height:54px;border-radius:50%;background:var(--c-primary);' + + 'border:none;color:#fff;cursor:pointer;z-index:200;' + + 'display:flex;align-items:center;justify-content:center;' + + 'box-shadow:0 4px 18px rgba(196,132,58,.45);transition:transform .12s,box-shadow .12s;' + + '-webkit-tap-highlight-color:transparent'; + fab.innerHTML = ''; + fab.addEventListener('click', () => App.navigate('wurfboerse')); + document.body.appendChild(fab); try { const p = await API.breeder.profile(zwingername); diff --git a/backend/static/sw.js b/backend/static/sw.js index 0ececf1..eea0518 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-v907'; +const CACHE_VERSION = 'by-v908'; 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 a577e6d8d92efb28bb04f913003d3c112869be77 Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 19:44:25 +0200 Subject: [PATCH 26/36] =?UTF-8?q?UX:=20Privater-Bereich-Header=20auf=20Zuc?= =?UTF-8?q?htkartei,=20Wurfverwaltung,=20L=C3=A4ufigkeit=20(SW=20by-v909)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/static/index.html | 8 +++---- backend/static/js/app.js | 2 +- backend/static/js/pages/laeufi.js | 23 ++++++++++++++++++++ backend/static/js/pages/litters.js | 30 +++++++++++++++++++++++++-- backend/static/js/pages/zuchthunde.js | 23 ++++++++++++++++++++ backend/static/sw.js | 2 +- 7 files changed, 81 insertions(+), 9 deletions(-) diff --git a/backend/main.py b/backend/main.py index e37b793..92aa68c 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 = "908" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "909" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index cc1f098..99b347f 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -591,10 +591,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index e3e08be..cde78e5 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 = '908'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '909'; // ← 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/laeufi.js b/backend/static/js/pages/laeufi.js index ffb86d0..755b54a 100644 --- a/backend/static/js/pages/laeufi.js +++ b/backend/static/js/pages/laeufi.js @@ -26,9 +26,32 @@ window.Page_laeufi = (() => { // ---------------------------------------------------------- // Grundstruktur // ---------------------------------------------------------- + function _privateHeader() { + return ` +
+
+ + + + + Privater Bereich + +
+
+ Nur du siehst das — nicht öffentlich +
+
`; + } + function _render() { _container.innerHTML = `
+ ${_privateHeader()}

${UI.icon('thermometer')} Läufigkeit & Trächtigkeit diff --git a/backend/static/js/pages/litters.js b/backend/static/js/pages/litters.js index e1502f4..56d0624 100644 --- a/backend/static/js/pages/litters.js +++ b/backend/static/js/pages/litters.js @@ -90,12 +90,38 @@ window.Page_litters = (() => { // ---------------------------------------------------------- // Grundstruktur rendern // ---------------------------------------------------------- + function _privateHeader(icon, title) { + const zwinger = _appState?.user?.name || ''; + return ` +
+
+ + + + + Privater Bereich + +
+
+
+ Nur du siehst das — nicht öffentlich +
+
+
`; + } + function _render() { _container.innerHTML = `
-
+ ${_privateHeader()} +

- ${UI.icon('dog')} Meine Würfe + ${UI.icon('certificate')} Meine Würfe

@@ -89,7 +89,7 @@ window.Page_laeufi = (() => { _renderHundeList(); } catch (err) { document.getElementById('laeufi-list').innerHTML = - `

${UI.esc(err.message || 'Fehler')}

`; + `

${UI.escape(err.message || 'Fehler')}

`; } } @@ -131,12 +131,12 @@ window.Page_laeufi = (() => { cursor:pointer;user-select:none">
- ${UI.esc(h.name)} - ${h.rufname ? `"${UI.esc(h.rufname)}"` : ''} + ${UI.escape(h.name)} + ${h.rufname ? `"${UI.escape(h.rufname)}"` : ''} ${alter ? `${alter}` : ''}
${h.rasse_text || h.farbe ? `
- ${[h.rasse_text, h.farbe].filter(Boolean).map(s => UI.esc(s)).join(' · ')} + ${[h.rasse_text, h.farbe].filter(Boolean).map(s => UI.escape(s)).join(' · ')}
` : ''}
${UI.icon('caret-down')} @@ -177,7 +177,7 @@ window.Page_laeufi = (() => { ]); _renderHundContent(el, hundId, laeufiList, deckList); } catch (err) { - el.innerHTML = `

${UI.esc(err.message || 'Fehler')}

`; + el.innerHTML = `

${UI.escape(err.message || 'Fehler')}

`; } } @@ -276,7 +276,7 @@ window.Page_laeufi = (() => { ${l.ende ? `→ ${_fmtDate(l.ende)} ${_daysDiff(l.beginn, l.ende)} Tage` : ''}
- ${l.notiz ? `

${UI.esc(l.notiz)}

` : ''} + ${l.notiz ? `

${UI.escape(l.notiz)}

` : ''}
- ${d.ruede_name ? `${UI.icon('dog')} Rüde: ${UI.esc(d.ruede_name)}` : ''} + ${d.ruede_name ? `${UI.icon('dog')} Rüde: ${UI.escape(d.ruede_name)}` : ''} ${UI.icon('arrows-clockwise')} ${_DECKART[d.deckart] || d.deckart} ${d.ultraschall_datum ? `${UI.icon('heartbeat')} Ultraschall: ${_fmtDate(d.ultraschall_datum)}` : ''}
@@ -360,7 +360,7 @@ window.Page_laeufi = (() => { ${_fmtDate(m.datum)} - ${UI.esc(m.label)} + ${UI.escape(m.label)}
`).join('')}
@@ -390,7 +390,7 @@ window.Page_laeufi = (() => {
- +
`, footer: ` @@ -439,7 +439,7 @@ window.Page_laeufi = (() => {
+ value="${UI.escape(v.ruede_name || '')}">
@@ -468,7 +468,7 @@ window.Page_laeufi = (() => {
- +
`, footer: ` @@ -537,10 +537,10 @@ window.Page_laeufi = (() => { ${_fmtDate(t.datum)} - ${t.wert != null ? `${t.wert} ${UI.esc(t.einheit)}` : '—'} + ${t.wert != null ? `${t.wert} ${UI.escape(t.einheit)}` : '—'} ${t.wert != null ? `${_progEinschaetzung(t.wert, t.einheit)}` : ''} - ${t.labor ? UI.esc(t.labor) : '—'} + ${t.labor ? UI.escape(t.labor) : '—'} diff --git a/backend/static/sw.js b/backend/static/sw.js index 3fcf3a7..91b4097 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-v916'; +const CACHE_VERSION = 'by-v917'; 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 3a3002aff3219983ca84b0f1b8a74313ddc4a942 Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 20:31:27 +0200 Subject: [PATCH 36/36] =?UTF-8?q?Fix:=20Welten-Config=20auto-merge=20neuer?= =?UTF-8?q?=20Default-Chips=20=E2=80=94=20L=C3=A4ufigkeit=20erscheint=20au?= =?UTF-8?q?tomatisch=20(SW=20by-v918)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/static/index.html | 8 ++++---- backend/static/js/app.js | 2 +- backend/static/js/worlds.js | 22 ++++++++++++++++++---- backend/static/sw.js | 2 +- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/backend/main.py b/backend/main.py index 164ff54..4bcd9ba 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 = "917" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "918" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index d411c33..0e13659 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 fc9d1de..c6d2189 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 = '917'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '918'; // ← 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 c95416c..0c3c15d 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -569,24 +569,38 @@ window.Worlds = (() => { // _cfgCache: wird beim Init aus DB geladen, Fallback localStorage → Default let _cfgCache = null; + function _mergeDefaults(cfg) { + // Neue Default-Chips die noch nicht in der gespeicherten Config sind → anhängen + const result = JSON.parse(JSON.stringify(cfg)); + for (const world of ['jetzt', 'hund', 'welt']) { + const def = _DEFAULT_CONFIG[world] || []; + const saved = result[world] || []; + for (const page of def) { + if (!saved.includes(page)) saved.push(page); + } + result[world] = saved; + } + return result; + } + async function _loadConfigFromServer() { try { const res = await API.get('/profile/world-config'); if (res?.config) { - _cfgCache = res.config; + _cfgCache = _mergeDefaults(res.config); try { localStorage.setItem('world_chips', JSON.stringify(_cfgCache)); } catch {} return; } // Noch nichts in DB: lokale Config hochladen (einmalige Migration) const local = (() => { try { return JSON.parse(localStorage.getItem('world_chips') || 'null'); } catch { return null; } })(); if (local) { - _cfgCache = local; - API.put('/profile/world-config', { config: local }).catch(() => {}); + _cfgCache = _mergeDefaults(local); + API.put('/profile/world-config', { config: _cfgCache }).catch(() => {}); return; } } catch {} // Fallback: localStorage → Default - try { _cfgCache = JSON.parse(localStorage.getItem('world_chips') || 'null') || _DEFAULT_CONFIG; } + try { _cfgCache = _mergeDefaults(JSON.parse(localStorage.getItem('world_chips') || 'null') || _DEFAULT_CONFIG); } catch { _cfgCache = _DEFAULT_CONFIG; } } diff --git a/backend/static/sw.js b/backend/static/sw.js index 91b4097..e5fe3f5 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-v917'; +const CACHE_VERSION = 'by-v918'; 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