Fix: Tagesübung nur JS-kompatible exercise_ids, Scroll per exercise_id; Landing+llms.txt Sprint-20 — SW by-v492, APP_VER 469

This commit is contained in:
rene 2026-04-29 11:34:28 +02:00
parent fe2e718827
commit 175984e80f
7 changed files with 78 additions and 36 deletions

View file

@ -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,

View file

@ -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 = (() => {

View file

@ -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 `
<div class="card" style="padding:0;overflow:hidden" data-exercise-name="${_esc(u.name)}">
<div class="card" style="padding:0;overflow:hidden" data-exercise-name="${_esc(u.name)}" data-exercise-id="${_esc(_progressKey(_activeTab, u.name))}">
<!-- Header -->
<div style="padding:var(--space-4) var(--space-4) var(--space-3)">
<!-- Zeile 1: Name + Schwierigkeits-Badge -->

View file

@ -364,7 +364,8 @@ window.Page_welcome = (() => {
${ex ? `
<div class="wc-chip" id="wc-chip-exercise"
data-exercise-name="${UI.escape(ex.name)}"
data-exercise-kat="${UI.escape(ex.kategorie || '')}">
data-exercise-kat="${UI.escape(ex.kategorie || '')}"
data-exercise-id="${UI.escape(ex.exercise_id || '')}">
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#target"></use></svg>
<span class="wc-chip-label">Übung des Tages</span>
<span class="wc-chip-val">${UI.escape(ex.name)}</span>
@ -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,
});
});
}

View file

@ -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 @@
<div class="feature-group">
<div class="feature-group-label">Mein Hund</div>
<div class="feature-grid">
<div class="feature-card">
<span class="feature-icon">🏠</span>
<div><h3>Personalisiertes Dashboard</h3><p>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.</p><span class="feature-tag">Kostenlos</span></div>
</div>
<div class="feature-card">
<span class="feature-icon">📓</span>
<div><h3>Tagebuch</h3><p>Fotos, Videos, Texte und GPS-Orte — alle Momente mit deinem Hund. Kategorien wie Spaziergänge, Meilensteine, Lustiges.</p><span class="feature-tag">Kostenlos</span></div>
@ -467,6 +474,10 @@
<span class="feature-icon">🐾</span>
<div><h3>GPS-Routen</h3><p>Routen aufzeichnen, teilen und bewerten — Untergrund, Schatten, Leinenpflicht, Sicherheit.</p><span class="feature-tag">Kostenlos</span></div>
</div>
<div class="feature-card">
<span class="feature-icon">🧭</span>
<div><h3>Tages-Gassirunde</h3><p>Täglich eine neue Rundroute vorgeschlagen — 2, 4 oder 6 km ab deinem Standort. Berechnet via OpenRouteService, direkt navigierbar. 3 verschiedene Varianten pro Tag.</p><span class="feature-tag">Kostenlos</span></div>
</div>
<div class="feature-card">
<span class="feature-icon">📅</span>
<div><h3>Events & Turniere</h3><p>Agility-Turniere, Hundeausstellungen und lokale Veranstaltungen in deiner Region.</p><span class="feature-tag">Kostenlos</span></div>
@ -690,6 +701,13 @@
<td class="cross"></td>
<td class="cross"></td>
</tr>
<tr>
<td>Täglicher Routenvorschlag (Gassirunde)</td>
<td class="check"></td>
<td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td>
</tr>
</tbody>
</table>
</div>

View file

@ -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

View file

@ -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