diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 9df065c..4bb9b97 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -159,34 +159,32 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)): "SELECT COUNT(*) AS n FROM diary WHERE dog_id=?", (dog_id,) ).fetchone()["n"] - # Tagesübung — personalisiert aus exercise_progress, tagesstabil + # Tagesübung — aus exercise_progress im JS-Format (tab_Name), tagesstabil import datetime as _dt day_num = (_dt.date.today() - _dt.date(2024, 1, 1)).days - # Übungen in Bearbeitung (noch-nicht / manchmal / meistens), älteste zuerst - in_progress = conn.execute( - """SELECT ep.exercise_id, te.name, te.kategorie, te.schwierigkeit - FROM exercise_progress ep - JOIN training_exercises te ON te.exercise_id = ep.exercise_id - WHERE ep.user_id = ? AND ep.status IN ('noch-nicht', 'manchmal', 'meistens') - ORDER BY ep.updated_at ASC LIMIT 20""", + # Nur exercise_ids im JS-Format (starten mit bekanntem Tab-Namen) + _KNOWN_PREFIXES = ('grundkommandos_', 'tricks_', 'problemverhalten_', 'grundlagen_') + raw_progress = conn.execute( + """SELECT exercise_id FROM exercise_progress + WHERE user_id = ? AND status IN ('noch-nicht', 'manchmal', 'meistens') + ORDER BY updated_at ASC LIMIT 50""", (user["id"],) ).fetchall() + valid = [r["exercise_id"] for r in raw_progress + if any(r["exercise_id"].startswith(p) for p in _KNOWN_PREFIXES)] + daily_exercise = None - if in_progress: - daily_exercise = dict(in_progress[day_num % len(in_progress)]) - else: - # Fallback: globale Rotation über alle Übungen - exercise_count = conn.execute( - "SELECT COUNT(*) AS n FROM training_exercises" - ).fetchone()["n"] - if exercise_count: - ex_row = conn.execute( - "SELECT exercise_id, name, kategorie, schwierigkeit FROM training_exercises ORDER BY id LIMIT 1 OFFSET ?", - (day_num % exercise_count,) - ).fetchone() - daily_exercise = dict(ex_row) if ex_row else None + if valid: + ex_id = valid[day_num % len(valid)] + # Tab + Name aus exercise_id ableiten + for prefix in _KNOWN_PREFIXES: + if ex_id.startswith(prefix): + tab = prefix.rstrip('_') + name = ex_id[len(prefix):].replace('_', ' ') + daily_exercise = {"exercise_id": ex_id, "name": name, "kategorie": tab} + break return { "random_photo": random_photo, diff --git a/backend/static/js/app.js b/backend/static/js/app.js index fe30461..1f19eed 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 = '468'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '469'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/uebungen.js b/backend/static/js/pages/uebungen.js index 40a8b3c..4ad56fc 100644 --- a/backend/static/js/pages/uebungen.js +++ b/backend/static/js/pages/uebungen.js @@ -448,14 +448,26 @@ window.Page_uebungen = (() => { async function init(container, appState, params = {}) { _container = container; _appState = appState; - if (params.kategorie) { + // Tab aus exercise_id (JS-Format) oder kategorie ableiten + const exId = params.exercise_id || ''; + if (exId) { + for (const [, tabId] of Object.entries(_KAT_TO_TAB)) { + if (exId.startsWith(tabId + '_') && _VALID_TABS.has(tabId)) { + _activeTab = tabId; break; + } + } + } else if (params.kategorie) { const mapped = _KAT_TO_TAB[params.kategorie.toLowerCase()] || params.kategorie; if (_VALID_TABS.has(mapped)) _activeTab = mapped; } _render(); - if (params.name) { + if (params.exercise_id || params.name) { setTimeout(() => { - const card = _container.querySelector(`[data-exercise-name="${CSS.escape(params.name)}"]`); + // Erst per exercise_id suchen (zuverlässig), dann per Name als Fallback + const card = (params.exercise_id + ? _container.querySelector(`[data-exercise-id="${CSS.escape(params.exercise_id)}"]`) + : null) + || _container.querySelector(`[data-exercise-name="${CSS.escape(params.name || '')}"]`); if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 700); } @@ -962,7 +974,7 @@ window.Page_uebungen = (() => { const hasBody = u.schritte.length > 0 || u.fehler.length > 0 || u.steigerung; return ` -
+
diff --git a/backend/static/js/pages/welcome.js b/backend/static/js/pages/welcome.js index a105a61..ded2178 100644 --- a/backend/static/js/pages/welcome.js +++ b/backend/static/js/pages/welcome.js @@ -364,7 +364,8 @@ window.Page_welcome = (() => { ${ex ? `
+ data-exercise-kat="${UI.escape(ex.kategorie || '')}" + data-exercise-id="${UI.escape(ex.exercise_id || '')}"> Übung des Tages ${UI.escape(ex.name)} @@ -534,8 +535,9 @@ window.Page_welcome = (() => { if (exChip) { exChip.addEventListener('click', () => { App.navigate('uebungen', true, { - name: exChip.dataset.exerciseName, - kategorie: exChip.dataset.exerciseKat, + name: exChip.dataset.exerciseName, + kategorie: exChip.dataset.exerciseKat, + exercise_id: exChip.dataset.exerciseId, }); }); } @@ -1154,8 +1156,9 @@ window.Page_welcome = (() => { if (exChip) { exChip.addEventListener('click', () => { App.navigate('uebungen', true, { - name: exChip.dataset.exerciseName, - kategorie: exChip.dataset.exerciseKat, + name: exChip.dataset.exerciseName, + kategorie: exChip.dataset.exerciseKat, + exercise_id: exChip.dataset.exerciseId, }); }); } diff --git a/backend/static/landing.html b/backend/static/landing.html index 77e3513..c92d835 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -78,11 +78,14 @@ "Probeverpaarung mit IK-Simulation und genetischer Risikoanalyse", "Tierschutz-Check automatisch bei jeder Verpaarung — nicht abschaltbar", "KI-Züchter-Assistenz: Wurfankündigungen, Genetik-Erklärung, Paarungsanalyse", - "Datenexport als HTML und ODS — keine Datenfalle" + "Datenexport als HTML und ODS — keine Datenfalle", + "Personalisierte Tagesroute via OpenRouteService — täglich neue Gassirunde mit 2/4/6 km Wahl", + "Übung des Tages — personalisiert aus dem persönlichen Trainingsfortschritt", + "Dashboard-Startseite mit Hundebild-Hero, Statistik-Chips und Feature-Karten" ], "screenshot": "https://banyaro.app/icons/icon-512.png", - "softwareVersion": "2.1", - "datePublished": "2026-04-28", + "softwareVersion": "2.2", + "datePublished": "2026-04-29", "areaServed": ["DE", "AT", "CH"], "audience": { "@type": "Audience", @@ -413,6 +416,10 @@
Mein Hund
+
+ 🏠 +

Personalisiertes Dashboard

Die Startseite begrüßt dich mit einem täglich wechselnden Foto deines Hundes aus dem Tagebuch, zeigt aktuelle Stats (letzter Eintrag, nächster Termin, Gewicht, Übung des Tages) und navigiert direkt zu allen Bereichen.

Kostenlos
+
📓

Tagebuch

Fotos, Videos, Texte und GPS-Orte — alle Momente mit deinem Hund. Kategorien wie Spaziergänge, Meilensteine, Lustiges.

Kostenlos
@@ -467,6 +474,10 @@ 🐾

GPS-Routen

Routen aufzeichnen, teilen und bewerten — Untergrund, Schatten, Leinenpflicht, Sicherheit.

Kostenlos
+
+ 🧭 +

Tages-Gassirunde

Täglich eine neue Rundroute vorgeschlagen — 2, 4 oder 6 km ab deinem Standort. Berechnet via OpenRouteService, direkt navigierbar. 3 verschiedene Varianten pro Tag.

Kostenlos
+
📅

Events & Turniere

Agility-Turniere, Hundeausstellungen und lokale Veranstaltungen in deiner Region.

Kostenlos
@@ -690,6 +701,13 @@ ✗ ✗ + + Täglicher Routenvorschlag (Gassirunde) + ✓ + ✗ + ✗ + ✗ +
diff --git a/backend/static/llms.txt b/backend/static/llms.txt index 02c4ec2..8694e2f 100644 --- a/backend/static/llms.txt +++ b/backend/static/llms.txt @@ -1,6 +1,6 @@ # Ban Yaro — Die deutschsprachige Hunde-Plattform # https://banyaro.app -# Letzte Aktualisierung: 2026-04-28 +# Letzte Aktualisierung: 2026-04-29 ## Was ist Ban Yaro? @@ -48,6 +48,7 @@ Ban Yaro ist kostenlos nutzbar (Freemium-Modell). Die App ist auf allen Smartpho - Virtueller KI-Trainer: analysiert letzte 20 Sessions, tägliche Empfehlung - Fortschrittsprognose bis zur Meisterschaft - Gamification: Streaks, Abzeichen, Trainingskalender +- Übung des Tages: täglich eine personalisierte Übungsempfehlung aus dem eigenen Fortschritt ### Züchter-Plattform (vollständig) @@ -114,6 +115,7 @@ Ban Yaro ist die erste Hunde-App mit vollständiger Züchter-Unterstützung: - Verlorener Hund Alarm - Gassi-Treffen organisieren und finden - GPS-Routen aufzeichnen, teilen, bewerten +- Tages-Gassirunde: täglich neue Rundroute via OpenRouteService (2/4/6 km), direkt navigierbar - Hundesitting-Netzwerk (nur 8% Provision vs. 20% bei Rover/Pawshake) - Forum mit Rassen-basierten Unterforen - Direktnachrichten / Chat @@ -135,6 +137,15 @@ Ban Yaro nutzt KI an mehreren Stellen: - **Symptom-Checker, KI-Trainer, Lober**: Für alle kostenfrei - **Züchter-KI**: Wurfankündigungen, Genetik-Erklärungen, Paarungsanalyse, Jahresbericht +## Dashboard & Personalisierung + +Die Startseite für eingeloggte Nutzer zeigt: +- Täglich wechselndes Foto des Hundes aus dem Tagebuch als Hero-Bild +- Stats-Chips: letzter Tagebucheintrag (relativ), nächster Gesundheitstermin (<60 Tage), Gewicht +- Übung des Tages — personalisiert aus dem eigenen Trainingsfortschritt +- Gassirunden-Vorschlag — tägliche Route via OpenRouteService wenn Standort freigegeben +- 4 Feature-Karten (Tagebuch, Gesundheit, Karte, Training) + vollständiges Feature-Grid + ## Technologie - Progressive Web App (PWA) — installierbar ohne App Store diff --git a/backend/static/sw.js b/backend/static/sw.js index 8918bc7..029415f 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-v491'; +const CACHE_VERSION = 'by-v492'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten