Feature: Alle 104 Übungen aus DB in Übungsseite — 9 Tabs, DB-basiert, abwärtskompatibel — SW by-v493, APP_VER 470

This commit is contained in:
rene 2026-04-29 11:44:47 +02:00
parent 175984e80f
commit 6d9f4a097e
5 changed files with 109 additions and 24 deletions

View file

@ -164,7 +164,11 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
day_num = (_dt.date.today() - _dt.date(2024, 1, 1)).days day_num = (_dt.date.today() - _dt.date(2024, 1, 1)).days
# Nur exercise_ids im JS-Format (starten mit bekanntem Tab-Namen) # Nur exercise_ids im JS-Format (starten mit bekanntem Tab-Namen)
_KNOWN_PREFIXES = ('grundkommandos_', 'tricks_', 'problemverhalten_', 'grundlagen_') _KNOWN_PREFIXES = (
'grundkommandos_', 'tricks_', 'problemverhalten_',
'mentale-auslastung_', 'hundesport_', 'koerperpflege_', 'welpe-basics_',
'grundlagen_',
)
raw_progress = conn.execute( raw_progress = conn.execute(
"""SELECT exercise_id FROM exercise_progress """SELECT exercise_id FROM exercise_progress
WHERE user_id = ? AND status IN ('noch-nicht', 'manchmal', 'meistens') WHERE user_id = ? AND status IN ('noch-nicht', 'manchmal', 'meistens')

View file

@ -10,6 +10,45 @@ from auth import get_current_user
router = APIRouter() router = APIRouter()
# ------------------------------------------------------------------
# Alle Übungen aus DB (öffentlich, kein Auth)
# ------------------------------------------------------------------
@router.get("/exercises")
async def get_exercises():
"""Alle Übungen aus der DB, gruppiert nach Tab-ID."""
import json as _json
CAT_TO_TAB = {
'Grundkommando': 'grundkommandos',
'Trick': 'tricks',
'Problemverhalten': 'problemverhalten',
'Mentale Auslastung': 'mentale-auslastung',
'Hundesport': 'hundesport',
'Koerperpflege': 'koerperpflege',
'Körperpflege': 'koerperpflege',
'Welpe Basics': 'welpe-basics',
}
with db() as conn:
rows = conn.execute("""
SELECT exercise_id, name, kategorie, schwierigkeit, alter_ab,
dauer, beschreibung, schritte, tipp
FROM training_exercises ORDER BY kategorie, name
""").fetchall()
by_tab = {}
for r in rows:
tab = CAT_TO_TAB.get(r['kategorie'], r['kategorie'].lower().replace(' ', '-'))
by_tab.setdefault(tab, []).append({
'exercise_id': r['exercise_id'],
'name': r['name'],
'kategorie': tab,
'schwierigkeit': r['schwierigkeit'] or 'mittel',
'alter': r['alter_ab'],
'dauer': r['dauer'],
'beschreibung': r['beschreibung'],
'schritte': _json.loads(r['schritte'] or '[]'),
'tipp': r['tipp'],
})
return by_tab
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Übungs-Status # Übungs-Status
# ------------------------------------------------------------------ # ------------------------------------------------------------------

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. Router, State-Management, Navigation, Initialisierung.
============================================================ */ ============================================================ */
const APP_VER = '469'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VER = '470'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => { const App = (() => {

View file

@ -36,17 +36,23 @@ window.Page_uebungen = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
let _statsData = null; // cached stats from /api/training/stats let _statsData = null; // cached stats from /api/training/stats
let _badgesData = null; // cached badges from /api/achievements let _badgesData = null; // cached badges from /api/achievements
let _exercisesByTab = {}; // aus API geladen
let _exercisesLoaded = false;
// ---------------------------------------------------------- // ----------------------------------------------------------
// DATEN // DATEN
// ---------------------------------------------------------- // ----------------------------------------------------------
const TABS = [ const TABS = [
{ id: 'grundkommandos', label: 'Grundkommandos' }, { id: 'grundkommandos', label: 'Grundkommandos' },
{ id: 'tricks', label: 'Tricks & Beschäftigung' }, { id: 'tricks', label: 'Tricks' },
{ id: 'problemverhalten', label: 'Problemverhalten' }, { id: 'problemverhalten', label: 'Problemverhalten' },
{ id: 'grundlagen', label: 'Trainingsgrundlagen' }, { id: 'mentale-auslastung', label: 'Mentale Auslastung' },
{ id: 'ki-trainer', label: 'KI-Trainer' }, { id: 'hundesport', label: 'Hundesport' },
{ id: 'koerperpflege', label: 'Körperpflege' },
{ id: 'welpe-basics', label: 'Welpe Basics' },
{ id: 'grundlagen', label: 'Trainingsgrundlagen' },
{ id: 'ki-trainer', label: 'KI-Trainer' },
]; ];
// ---------------------------------------------------------- // ----------------------------------------------------------
@ -435,13 +441,20 @@ window.Page_uebungen = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// DB-Kategorien → Tab-IDs // DB-Kategorien → Tab-IDs
const _KAT_TO_TAB = { const _KAT_TO_TAB = {
'grundkommando': 'grundkommandos', 'grundkommando': 'grundkommandos',
'grundkommandos': 'grundkommandos', 'grundkommandos': 'grundkommandos',
'trick': 'tricks', 'trick': 'tricks',
'tricks': 'tricks', 'tricks': 'tricks',
'problemverhalten':'problemverhalten', 'problemverhalten': 'problemverhalten',
'grundlagen': 'grundlagen', 'mentale auslastung': 'mentale-auslastung',
'ki-trainer': 'ki-trainer', 'mentale-auslastung': 'mentale-auslastung',
'hundesport': 'hundesport',
'körperpflege': 'koerperpflege',
'koerperpflege': 'koerperpflege',
'welpe basics': 'welpe-basics',
'welpe-basics': 'welpe-basics',
'grundlagen': 'grundlagen',
'ki-trainer': 'ki-trainer',
}; };
const _VALID_TABS = new Set(TABS.map(t => t.id)); const _VALID_TABS = new Set(TABS.map(t => t.id));
@ -461,6 +474,16 @@ window.Page_uebungen = (() => {
if (_VALID_TABS.has(mapped)) _activeTab = mapped; if (_VALID_TABS.has(mapped)) _activeTab = mapped;
} }
_render(); _render();
// Übungen aus DB laden (parallel mit Progress)
if (!_exercisesLoaded) {
API.get('/training/exercises').then(data => {
_exercisesByTab = data || {};
_exercisesLoaded = true;
_renderContent(); // neu rendern sobald Daten da
}).catch(() => {});
}
if (params.exercise_id || params.name) { if (params.exercise_id || params.name) {
setTimeout(() => { setTimeout(() => {
// Erst per exercise_id suchen (zuverlässig), dann per Name als Fallback // Erst per exercise_id suchen (zuverlässig), dann per Name als Fallback
@ -911,7 +934,8 @@ window.Page_uebungen = (() => {
const el = _container.querySelector('#ueb-content'); const el = _container.querySelector('#ueb-content');
if (!el) return; if (!el) return;
const isExerciseTab = ['grundkommandos', 'tricks', 'problemverhalten'].includes(_activeTab); const isExerciseTab = ['grundkommandos','tricks','problemverhalten',
'mentale-auslastung','hundesport','koerperpflege','welpe-basics'].includes(_activeTab);
const showIf = v => v ? '' : 'none'; const showIf = v => v ? '' : 'none';
const quickWrap = _container.querySelector('#ueb-quicksetup-btn')?.parentElement; const quickWrap = _container.querySelector('#ueb-quicksetup-btn')?.parentElement;
@ -924,11 +948,23 @@ window.Page_uebungen = (() => {
if (bannerEl) bannerEl.style.display = showIf(isExerciseTab); if (bannerEl) bannerEl.style.display = showIf(isExerciseTab);
switch (_activeTab) { switch (_activeTab) {
case 'grundkommandos': el.innerHTML = _renderUebungsList(GRUNDKOMMANDOS); break; case 'grundkommandos':
case 'tricks': el.innerHTML = _renderUebungsList(TRICKS); break; case 'tricks':
case 'problemverhalten': el.innerHTML = _renderUebungsList(PROBLEMVERHALTEN); break; case 'problemverhalten':
case 'grundlagen': el.innerHTML = _renderGrundlagen(); break; case 'mentale-auslastung':
case 'ki-trainer': el.innerHTML = _renderKiTrainer(); break; case 'hundesport':
case 'koerperpflege':
case 'welpe-basics': {
const list = _exercisesByTab[_activeTab] || [];
el.innerHTML = list.length
? _renderUebungsList(list)
: `<div style="padding:var(--space-6);text-align:center;color:var(--c-text-muted)">
${UI.icon('spinner')} Übungen werden geladen
</div>`;
break;
}
case 'grundlagen': el.innerHTML = _renderGrundlagen(); break;
case 'ki-trainer': el.innerHTML = _renderKiTrainer(); break;
} }
_bindAccordions(); _bindAccordions();
_bindStatusButtons(); _bindStatusButtons();
@ -971,7 +1007,7 @@ window.Page_uebungen = (() => {
const currentId = _getStatus(_activeTab, u.name); const currentId = _getStatus(_activeTab, u.name);
const sm = _statusMeta(currentId); const sm = _statusMeta(currentId);
const hasBody = u.schritte.length > 0 || u.fehler.length > 0 || u.steigerung; const hasBody = (u.schritte?.length > 0) || (u.fehler?.length > 0) || !!u.steigerung || !!u.tipp;
return ` return `
<div class="card" style="padding:0;overflow:hidden" data-exercise-name="${_esc(u.name)}" data-exercise-id="${_esc(_progressKey(_activeTab, u.name))}"> <div class="card" style="padding:0;overflow:hidden" data-exercise-name="${_esc(u.name)}" data-exercise-id="${_esc(_progressKey(_activeTab, u.name))}">
@ -1078,7 +1114,7 @@ window.Page_uebungen = (() => {
</svg> </svg>
</button> </button>
<div id="${uid}" hidden style="padding:var(--space-4);border-top:1px solid var(--c-border)"> <div id="${uid}" hidden style="padding:var(--space-4);border-top:1px solid var(--c-border)">
${u.schritte.length ? ` ${u.schritte?.length ? `
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold); <p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em"> color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
Schritt für Schritt Schritt für Schritt
@ -1089,7 +1125,7 @@ window.Page_uebungen = (() => {
`).join('')} `).join('')}
</ol> </ol>
` : ''} ` : ''}
${u.fehler.length ? ` ${u.fehler?.length ? `
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold); <p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em"> color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
<svg class="ph-icon" style="width:12px;height:12px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> <svg class="ph-icon" style="width:12px;height:12px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
@ -1110,6 +1146,12 @@ window.Page_uebungen = (() => {
<strong>Steigerung:</strong> ${_esc(u.steigerung)} <strong>Steigerung:</strong> ${_esc(u.steigerung)}
</div> </div>
` : ''} ` : ''}
${u.tipp ? `
<div style="margin-top:var(--space-3);padding:var(--space-2) var(--space-3);
background:var(--c-primary-subtle);border-radius:var(--radius-sm);
font-size:var(--text-xs);color:var(--c-text-secondary)">
💡 ${_esc(u.tipp)}
</div>` : ''}
</div> </div>
` : ''} ` : ''}
</div> </div>

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache Offline-Cache + Push Notifications + Tile-Cache
============================================================ */ ============================================================ */
const CACHE_VERSION = 'by-v492'; const CACHE_VERSION = 'by-v493';
const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten