Feature: Trauer-Feature, Futter-Verträglichkeit, Multi-Hund-Fixes, Wetter-Ort (Sprint 47)

- dog-profile.js: Verstorben-Button, Gedenkseite, KI-Abschiedstext
- database.py: futter_eintraege/reaktionen, route_dogs, exercise_progress.dog_id
- routes/ernaehrung.py: Futter-Verträglichkeit mit 20 Reaktionstypen + Analyse
- routes/routen.py: route_dogs Many-to-Many, Routen editierbar
- routes/training.py: exercise_progress per dog_id
- routes/ki.py: /ki/abschied Trauer-KI
- weather.py: Nominatim Ortsname parallel geladen
- ui.js: dogChip/bindDogChip, visualViewport-Modal
- api.js: gedenken, gedenkseite, futter-Methoden, route_dogs
- worlds.js: Ortsname im Wetter-Chip
- uebungen.js: _progressLoaded-Flag, dog-spezifischer Fortschritt
- trainingsplaene.js: dog_id Unterstützung
- diary.js/health.js: P-Badge Cleanup
- map.js: Wetter-Ort-Anzeige entfernt
- wetter.js: Ort in Wetter-Detail
This commit is contained in:
rene 2026-05-11 19:28:38 +02:00
parent 1ce802c8dc
commit bda61a0e40
16 changed files with 713 additions and 181 deletions

View file

@ -75,6 +75,7 @@ window.Page_uebungen = (() => {
// In-memory cache (loaded from API on init)
let _progressCache = {}; // key → statusId
let _progressLoaded = false;
let _exerciseStats = {}; // exercise_id → {recent_avg, session_count, trend}
function _progressKey(tab, name) {
@ -83,17 +84,13 @@ window.Page_uebungen = (() => {
function _getStatus(tab, name) {
const k = _progressKey(tab, name);
// Fallback to localStorage while API loads
return _progressCache[k] !== undefined
? _progressCache[k]
: localStorage.getItem(_statusKey(tab, name)) || null;
return _progressCache[k] ?? null;
}
function _setStatus(tab, name, statusId) {
const k = _progressKey(tab, name);
_progressCache[k] = statusId;
localStorage.setItem(_statusKey(tab, name), statusId || ''); // keep localStorage in sync
API.training.setProgress(k, statusId).catch(() => {});
API.training.setProgress(k, statusId, _dogId()).catch(() => {});
}
function _nextStatus(currentId) {
@ -504,28 +501,19 @@ window.Page_uebungen = (() => {
_scrollTarget = { exercise_id: params.exercise_id || '', name: params.name || '' };
}
// Progress vom Server laden
API.training.getProgress().then(rows => {
rows.forEach(r => { _progressCache[r.exercise_id] = r.status; });
// localStorage-Daten migrieren falls noch nicht im Backend
Object.keys(localStorage).filter(k => k.startsWith('ub_status_')).forEach(lsKey => {
const parts = lsKey.replace('ub_status_', '').split('_');
const tab = parts[0];
const name = parts.slice(1).join('_');
const apiKey = `${tab}_${name}`;
if (_progressCache[apiKey] === undefined) {
const val = localStorage.getItem(lsKey);
if (val) {
_progressCache[apiKey] = val;
API.training.setProgress(apiKey, val).catch(() => {});
}
}
});
_renderContent(); // Re-render with loaded progress
}).catch(() => {});
// Progress vom Server laden (hund-spezifisch)
const _did = _dogId();
_progressLoaded = false;
API.training.getProgress(_did)
.then(rows => {
_progressCache = {};
rows.forEach(r => { _progressCache[r.exercise_id] = r.status; });
_progressLoaded = true;
_renderContent();
}).catch(() => { _progressLoaded = true; _renderContent(); });
// Empfehlungen laden
API.training.getSuggestions().then(suggestions => {
API.training.getSuggestions(_did).then(suggestions => {
if (suggestions.length) _showSuggestions(suggestions);
}).catch(() => {});
@ -556,6 +544,7 @@ window.Page_uebungen = (() => {
_statsData = null;
_badgesData = null;
_progressCache = {};
_progressLoaded = false;
_exerciseStats = {};
_render();
_loadStatsAndBadges();
@ -568,6 +557,7 @@ window.Page_uebungen = (() => {
function _render() {
_container.innerHTML = `
<div id="ueb-wrap">
<div style="padding:var(--space-3) var(--space-4) 0">${UI.dogChip(_appState)}</div>
<div style="padding:var(--space-3) var(--space-4) var(--space-2)">
<table style="width:100%;border-collapse:collapse">
<tr>
@ -604,6 +594,7 @@ window.Page_uebungen = (() => {
<div id="ueb-content"></div>
</div>
`;
UI.bindDogChip(_container, _appState);
_container.querySelector('#ueb-quicksetup-btn').addEventListener('click', _openQuickSetupModal);
_container.querySelector('#ueb-tabs')?.style.setProperty('--ueb-tab-cols', Math.ceil(TABS.length / 2));
_container.querySelector('#ueb-search')?.addEventListener('input', e => {
@ -613,7 +604,12 @@ window.Page_uebungen = (() => {
_renderContent();
});
_bindTabs();
_renderContent();
if (_progressLoaded) {
_renderContent();
} else {
const el = _container.querySelector('#ueb-content');
if (el) el.innerHTML = `<div style="padding:var(--space-6);text-align:center;color:var(--c-text-muted)"><svg class="ph-icon" style="width:24px;height:24px;animation:spin 1s linear infinite" aria-hidden="true"><use href="/icons/phosphor.svg#spinner"></use></svg></div>`;
}
_renderStatsBanner();
}
@ -782,7 +778,18 @@ window.Page_uebungen = (() => {
// ----------------------------------------------------------
// SCHNELL-SETUP: Stand aller Übungen erfassen
// ----------------------------------------------------------
function _openQuickSetupModal() {
async function _openQuickSetupModal() {
// Sicherstellen dass Progress geladen ist bevor das Modal öffnet
if (!_progressLoaded) {
const did = _dogId();
try {
const rows = await API.training.getProgress(did);
_progressCache = {};
rows.forEach(r => { _progressCache[r.exercise_id] = r.status; });
_progressLoaded = true;
_renderContent();
} catch { _progressLoaded = true; }
}
const ALL = [
{ group: 'Grundkommandos', tab: 'grundkommandos', items: GRUNDKOMMANDOS },
{ group: 'Tricks', tab: 'tricks', items: TRICKS },
@ -883,11 +890,8 @@ window.Page_uebungen = (() => {
// Alle geänderten Status speichern
const parts = Object.entries(pending).map(([key, val]) => {
const [tab, ...rest] = key.split('_');
const name = rest.join('_').replace(/_/g, ' ');
_progressCache[key] = val || null;
localStorage.setItem(`ub_status_${key}`, val || '');
return API.training.setProgress(key, val || null);
return API.training.setProgress(key, val || null, _dogId());
});
await Promise.allSettled(parts);