Sprint 12+13: Tagebuch Day-One-Redesign, Notiz-Feature, Icon-Fixes, SW by-v405

Tagebuch:
- Day-One-Listenansicht: Wochentag + Tageszahl + Meta-Zeile (Zeit/Ort/Wetter)
- 4 Ansichten: Liste, Medien-Mosaik, Kalender (mit Sprungbuttons), Karte (GPS-Marker)
- Detail-Ansicht inline im Content-Bereich (kein Fullscreen-Overlay mehr)
- Hero-Bild vollständig sichtbar (object-fit:contain), Lightbox mit Safe-Area
- 2-Spalten-Layout Desktop: Text + Leaflet-Karte + POI-Liste
- EXIF-GPS-Extraktion bei Foto-Upload, historisches Wetter via Archive-API
- NoteStation-Import: Fotos in diary_media (80 Einträge migriert, 94 Medien)
- Stats-Endpoints: /diary/stats, /diary/calendar, /diary/locations

Notiz-Feature:
- Generische notes-Tabelle (parent_type + parent_id + meta_json)
- 📝-Button in 8 Bereichen, Notizblock-Seite mit KI-Analyse
- KI-Toggle in Einstellungen, notes_ki_enabled in User-Profil

Icons & Design:
- fill:currentColor Fix für welcome/onboarding/friends.js
- --c-icon Variable, --c-text-muted Dark Mode aufgehellt
- 15+ neue Phosphor-Icons aus lokaler Kopie
- CSS Network-First im SW, Cache-Control-Middleware

Infrastruktur:
- Wiki-Anreicherungs-Scheduler-Jobs entfernt (abgeschlossen)
- auth.py: notes_ki_enabled + is_social_media im User-Response
This commit is contained in:
rene 2026-04-25 20:44:46 +02:00
parent 95f91fdc00
commit 553e9e7854
35 changed files with 4558 additions and 370 deletions

View file

@ -895,6 +895,7 @@ window.Page_uebungen = (() => {
_bindAccordions();
_bindStatusButtons();
_bindLogButtons();
_bindNotizButtons();
if (_activeTab === 'ki-trainer') _loadKiTrainerFeedback();
}
@ -965,6 +966,19 @@ window.Page_uebungen = (() => {
Einheit
</button>
${_sessionStatsChip(_activeTab, u.name)}
<button class="ueb-notiz-btn"
data-tab="${_esc(_activeTab)}"
data-name="${_esc(u.name)}"
title="Notiz hinzufügen"
style="background:none;border:1px solid var(--c-border);cursor:pointer;
padding:3px 7px;border-radius:var(--radius-sm);
display:flex;align-items:center;gap:3px;
font-size:var(--text-xs);color:var(--c-text-secondary)">
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0" aria-hidden="true">
<use href="/icons/phosphor.svg#note-pencil"></use>
</svg>
Notiz
</button>
<button class="ueb-status-btn"
data-tab="${_esc(_activeTab)}"
data-name="${_esc(u.name)}"
@ -1006,7 +1020,7 @@ window.Page_uebungen = (() => {
background:#78350f22;border:1px solid #d9770644;border-radius:var(--radius-sm);
font-size:var(--text-xs);color:var(--c-text);line-height:1.4;
display:flex;align-items:flex-start;gap:var(--space-2)">
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0;margin-top:1px;color:#f59e0b" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0;margin-top:1px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
<span>${_esc(u.hinweis)}</span>
</div>
` : ''}
@ -1039,7 +1053,7 @@ window.Page_uebungen = (() => {
${u.fehler.length ? `
<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">
<svg class="ph-icon" style="width:12px;height:12px;color:#f59e0b" 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>
Häufige Fehler
</p>
<ul style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
@ -1100,6 +1114,252 @@ window.Page_uebungen = (() => {
});
}
function _bindNotizButtons() {
_container.querySelectorAll('.ueb-notiz-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const exerciseId = `${btn.dataset.tab}_${btn.dataset.name.replace(/[\s/]+/g, '_')}`;
_openNotizModal(exerciseId, btn.dataset.name, btn);
});
});
}
async function _openNotizModal(exerciseId, exerciseName, triggerBtn) {
const modalId = 'ueb-notiz-modal';
document.getElementById(modalId)?.remove();
// Lade bestehende Notiz
let existingNote = null;
if (_appState?.user) {
try {
const notes = await API.notes.get('training_session', exerciseId.length > 0 ? exerciseId : 0);
if (notes && notes.length > 0) existingNote = notes[0];
} catch (_) {}
}
const overlay = document.createElement('div');
overlay.id = modalId;
overlay.style.cssText = `
position:fixed;inset:0;z-index:9999;
display:flex;align-items:flex-end;justify-content:center;
background:rgba(0,0,0,0.45);
`;
const noteText = existingNote?.text || '';
const meta = existingNote?.meta_json || {};
const currentErfolgsquote = meta.erfolgsquote || null;
const currentUmgebung = meta.umgebung || null;
const currentStimmung = meta.hund_stimmung || null;
overlay.innerHTML = `
<div style="background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
width:100%;max-width:480px;max-height:85vh;overflow-y:auto;
padding:var(--space-5) var(--space-4) var(--space-6);box-shadow:0 -4px 24px rgba(0,0,0,0.15)">
<div style="width:40px;height:4px;background:var(--c-border);border-radius:2px;margin:0 auto var(--space-4)"></div>
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);
margin:0 0 var(--space-4);text-align:center">
Notiz: ${_esc(exerciseName)}
</h3>
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
<!-- Freitext -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Notiz</label>
<textarea id="ueb-notiz-text" rows="3"
placeholder="Was ist dir aufgefallen? Tipps für nächstes Mal…"
style="width:100%;padding:var(--space-3);border:1.5px solid var(--c-border);
border-radius:var(--radius-md);font-size:var(--text-sm);
font-family:var(--font-sans);background:var(--c-surface);
color:var(--c-text);resize:vertical;outline:none;
line-height:1.5">${_esc(noteText)}</textarea>
</div>
<!-- Erfolgsquote -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Bewertung (optional)</label>
<div style="display:flex;gap:var(--space-2)">
${[1,2,3,4,5].map(n => `
<button type="button" class="ueb-notiz-pfote" data-val="${n}"
style="font-size:1.4rem;border:1.5px solid var(--c-border);
border-radius:var(--radius-md);padding:4px 10px;cursor:pointer;
background:${currentErfolgsquote === n ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
border-color:${currentErfolgsquote === n ? 'var(--c-primary)' : 'var(--c-border)'};
transition:all 0.15s">🐾</button>
`).join('')}
</div>
</div>
<!-- Umgebung -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Umgebung (optional)</label>
<div style="display:flex;gap:var(--space-2)">
${[['🏠','zuhause'],['🌿','natur'],['🌆','stadt']].map(([emoji, val]) => `
<button type="button" class="ueb-notiz-umgebung" data-val="${val}"
style="font-size:1.2rem;border:1.5px solid var(--c-border);
border-radius:var(--radius-md);padding:4px 12px;cursor:pointer;
background:${currentUmgebung === val ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
border-color:${currentUmgebung === val ? 'var(--c-primary)' : 'var(--c-border)'};
transition:all 0.15s">${emoji}</button>
`).join('')}
</div>
</div>
<!-- Hund-Stimmung -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Stimmung des Hundes (optional)</label>
<div style="display:flex;gap:var(--space-2)">
${[['😊','super'],['😐','ok'],['😔','mude']].map(([emoji, val]) => `
<button type="button" class="ueb-notiz-stimmung" data-val="${val}"
style="font-size:1.2rem;border:1.5px solid var(--c-border);
border-radius:var(--radius-md);padding:4px 12px;cursor:pointer;
background:${currentStimmung === val ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
border-color:${currentStimmung === val ? 'var(--c-primary)' : 'var(--c-border)'};
transition:all 0.15s">${emoji}</button>
`).join('')}
</div>
</div>
</div>
<!-- Buttons -->
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-5)">
${existingNote ? `
<button id="ueb-notiz-delete" type="button"
style="padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);
border:1.5px solid var(--c-danger);background:none;
color:var(--c-danger);font-size:var(--text-sm);cursor:pointer">
Löschen
</button>
` : ''}
<button id="ueb-notiz-cancel" type="button"
style="flex:1;padding:var(--space-3);border-radius:var(--radius-md);
border:1.5px solid var(--c-border);background:none;
color:var(--c-text-secondary);font-size:var(--text-sm);cursor:pointer">
Abbrechen
</button>
<button id="ueb-notiz-save" type="button"
style="flex:2;padding:var(--space-3);border-radius:var(--radius-md);
border:none;background:var(--c-primary);
color:#fff;font-size:var(--text-sm);font-weight:var(--weight-semibold);cursor:pointer">
${existingNote ? 'Aktualisieren' : 'Speichern'}
</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// State
let selectedErfolgsquote = currentErfolgsquote;
let selectedUmgebung = currentUmgebung;
let selectedStimmung = currentStimmung;
// Pfoten-Buttons
overlay.querySelectorAll('.ueb-notiz-pfote').forEach(btn => {
btn.addEventListener('click', () => {
const val = parseInt(btn.dataset.val, 10);
selectedErfolgsquote = selectedErfolgsquote === val ? null : val;
overlay.querySelectorAll('.ueb-notiz-pfote').forEach(b => {
const active = parseInt(b.dataset.val, 10) === selectedErfolgsquote;
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
});
});
});
// Umgebung-Buttons
overlay.querySelectorAll('.ueb-notiz-umgebung').forEach(btn => {
btn.addEventListener('click', () => {
selectedUmgebung = selectedUmgebung === btn.dataset.val ? null : btn.dataset.val;
overlay.querySelectorAll('.ueb-notiz-umgebung').forEach(b => {
const active = b.dataset.val === selectedUmgebung;
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
});
});
});
// Stimmung-Buttons
overlay.querySelectorAll('.ueb-notiz-stimmung').forEach(btn => {
btn.addEventListener('click', () => {
selectedStimmung = selectedStimmung === btn.dataset.val ? null : btn.dataset.val;
overlay.querySelectorAll('.ueb-notiz-stimmung').forEach(b => {
const active = b.dataset.val === selectedStimmung;
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
});
});
});
function _closeNotizModal() {
overlay.remove();
}
overlay.addEventListener('click', e => { if (e.target === overlay) _closeNotizModal(); });
overlay.querySelector('#ueb-notiz-cancel').addEventListener('click', _closeNotizModal);
// Speichern
overlay.querySelector('#ueb-notiz-save').addEventListener('click', async () => {
const text = overlay.querySelector('#ueb-notiz-text').value.trim();
if (!text) { UI.toast.warning('Bitte gib eine Notiz ein.'); return; }
const saveBtn = overlay.querySelector('#ueb-notiz-save');
saveBtn.disabled = true;
saveBtn.textContent = 'Speichern…';
const meta = {};
if (selectedErfolgsquote) meta.erfolgsquote = selectedErfolgsquote;
if (selectedUmgebung) meta.umgebung = selectedUmgebung;
if (selectedStimmung) meta.hund_stimmung = selectedStimmung;
const payload = {
text,
meta_json: Object.keys(meta).length > 0 ? meta : null,
};
try {
if (existingNote) {
await API.notes.update(existingNote.id, payload);
} else {
await API.notes.create('training_session', exerciseId, payload);
}
_closeNotizModal();
UI.toast.success('Notiz gespeichert.');
// Notiz-Button leicht hervorheben
if (triggerBtn) {
triggerBtn.style.borderColor = 'var(--c-primary)';
triggerBtn.style.color = 'var(--c-primary)';
}
} catch (err) {
saveBtn.disabled = false;
saveBtn.textContent = existingNote ? 'Aktualisieren' : 'Speichern';
UI.toast.error('Speichern fehlgeschlagen.');
}
});
// Löschen
overlay.querySelector('#ueb-notiz-delete')?.addEventListener('click', async () => {
if (!existingNote) return;
try {
await API.notes.delete(existingNote.id);
_closeNotizModal();
UI.toast.success('Notiz gelöscht.');
if (triggerBtn) {
triggerBtn.style.borderColor = '';
triggerBtn.style.color = '';
}
} catch (_) {
UI.toast.error('Löschen fehlgeschlagen.');
}
});
}
function _openLogModal(tab, exerciseName, initialReps) {
// Build the modal HTML
const modalId = 'ueb-log-modal';