diff --git a/backend/ki.py b/backend/ki.py index b2224f9..47ae257 100644 --- a/backend/ki.py +++ b/backend/ki.py @@ -220,13 +220,15 @@ Antworte NUR als JSON: user_is_premium=user_is_premium, json_mode=True, ) - import json + import json, re + # Cloud-Modelle wrappen JSON manchmal in ```json … ``` — stripppen + cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", result.strip(), flags=re.DOTALL) try: - return json.loads(result) + return json.loads(cleaned) except json.JSONDecodeError: return { "dringlichkeit": "tierarzt_heute", - "einschaetzung": result, + "einschaetzung": cleaned, "hinweise": [], "zum_tierarzt_wenn": "Bei Verschlechterung sofort.", } diff --git a/backend/routes/diary.py b/backend/routes/diary.py index e2ead8f..abd2b35 100644 --- a/backend/routes/diary.py +++ b/backend/routes/diary.py @@ -214,6 +214,18 @@ async def list_diary(dog_id: int, limit: int = 20, offset: int = 0, user=Depends(get_current_user)): with db() as conn: _can_read_dog(dog_id, user["id"], conn) + # Sitter darf keine bestehenden Einträge lesen + dog = conn.execute("SELECT user_id FROM dogs WHERE id=?", (dog_id,)).fetchone() + is_owner = dog and dog["user_id"] == user["id"] + if not is_owner: + # Prüfen ob geteilter Hund (dog_shares) — darf lesen + shared = conn.execute( + """SELECT 1 FROM dog_shares WHERE dog_id=? AND shared_with_id=? AND accepted_at IS NOT NULL""", + (dog_id, user["id"]) + ).fetchone() + if not shared: + # Weder Besitzer noch geteilter Nutzer → Sitter → leere Liste + return [] extra = "AND (d.is_milestone=1 OR d.typ='meilenstein')" if milestone else "" if q: pattern = f"%{q}%" diff --git a/backend/routes/moderation.py b/backend/routes/moderation.py index c4a3aee..9c00d6f 100644 --- a/backend/routes/moderation.py +++ b/backend/routes/moderation.py @@ -105,6 +105,8 @@ async def mod_users( offset: int = 0, user=Depends(require_moderator), ): + is_admin = user["rolle"] == "admin" + with db() as conn: where = "WHERE 1=1" params = [] @@ -114,8 +116,12 @@ async def mod_users( if banned: where += " AND is_banned=1" + # Moderatoren sehen keine Admins + if not is_admin: + where += " AND rolle != 'admin' AND COALESCE(is_admin, 0) = 0" + # E-Mail nur für Admins; Moderatoren sehen maskierte Version - email_col = "email" if user["rolle"] == "admin" else \ + email_col = "email" if is_admin else \ "SUBSTR(email,1,2)||'***@'||SUBSTR(email,INSTR(email,'@')+1) AS email" rows = conn.execute(f""" @@ -145,12 +151,15 @@ async def mod_patch_user(uid: int, data: dict, user=Depends(require_moderator)): with db() as conn: target = conn.execute( - "SELECT id, rolle, name FROM users WHERE id=?", (uid,) + "SELECT id, rolle, is_admin, name FROM users WHERE id=?", (uid,) ).fetchone() if not target: raise HTTPException(404, "User nicht gefunden.") - if target["rolle"] == "admin" and user["rolle"] != "admin": - raise HTTPException(403, "Admins können nur von Admins verwaltet werden.") + # Moderatoren dürfen keine Admins bearbeiten + if user["rolle"] != "admin" and ( + target["rolle"] == "admin" or target["is_admin"] + ): + raise HTTPException(403, "Admins können nicht von Moderatoren bearbeitet werden.") cols = ", ".join(f"{k}=?" for k in updates) conn.execute( diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 875b2f7..7c1d1e6 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -4174,6 +4174,11 @@ html.modal-open { .forum-category-tabs { padding-bottom: var(--space-1); } +.forum-category-tabs .by-tab { + overflow: hidden; + text-overflow: ellipsis; + max-width: 10rem; /* prevents single pill from being wider than ~160px on mobile */ +} /* Category badge (colored pill) */ .forum-category-badge { diff --git a/backend/static/css/layout.css b/backend/static/css/layout.css index 27f85d6..5b5f4d9 100644 --- a/backend/static/css/layout.css +++ b/backend/static/css/layout.css @@ -653,6 +653,8 @@ justify-content: center; text-align: center; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } /* Gesundheit: Tabs auf 2 Zeilen */ diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 08b7828..5376d6c 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 = '404'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '407'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index bac3d5c..9758d7c 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -736,6 +736,19 @@ window.Page_diary = (() => { const listEl = _container.querySelector('#diary-list'); if (!listEl) return; + const dog = _appState.activeDog; + const isSitter = dog?.is_guest === true; + + // Sitter: Einträge grundsätzlich ausgeblendet — nur Hinweis + FAB bleibt aktiv + if (isSitter) { + listEl.innerHTML = UI.emptyState({ + icon: UI.icon('lock-simple'), + title: 'Einträge nicht sichtbar', + text: 'Du kannst neue Einträge hinzufügen, aber keine bestehenden Einträge sehen.', + }); + return; + } + if (_entries.length === 0) { listEl.innerHTML = UI.emptyState({ icon: UI.icon('book-open'), @@ -748,6 +761,16 @@ window.Page_diary = (() => { return; } + // Datenschutz-Hinweis: Einträge sind privat + const privacyNotice = ` +
+ + Deine Tagebucheinträge sind privat — nur du kannst sie sehen. +
`; + // Gruppieren nach Jahr-Monat (Anzeigereihenfolge: chronologisch absteigend) const groups = new Map(); _entries.forEach(e => { @@ -756,7 +779,7 @@ window.Page_diary = (() => { groups.get(key).push(e); }); - let html = ''; + let html = privacyNotice; groups.forEach((items, key) => { const monthLabel = key === 'unbekannt' ? 'Datum unbekannt' : _formatMonth(key); html += `
${monthLabel}
`; diff --git a/backend/static/js/pages/health.js b/backend/static/js/pages/health.js index 2811d47..48ce1e8 100644 --- a/backend/static/js/pages/health.js +++ b/backend/static/js/pages/health.js @@ -1209,9 +1209,16 @@ window.Page_health = (() => { if (!_data[t]) _data[t] = []; _data[t].unshift(saved); UI.toast.success('Eintrag erstellt.'); - if (t === 'gewicht' && saved.wert) { - _appState.activeDog.gewicht_kg = saved.wert; - } + } + + // Gewicht im App-State aktualisieren (für neuen Eintrag UND bei Bearbeitung) + if (t === 'gewicht' && saved.wert) { + _appState.activeDog.gewicht_kg = saved.wert; + _appState.dogs = _appState.dogs.map(d => + d.id === _appState.activeDog.id + ? { ...d, gewicht_kg: saved.wert } + : d + ); } // Multi-File-Upload @@ -1767,11 +1774,13 @@ window.Page_health = (() => { } const DRINGLICHKEIT = { - beobachten: { badgeClass: 'badge-success', label: '🟢 Beobachten' }, - tierarzt: { badgeClass: 'badge-warning', label: '🟡 Zum Tierarzt' }, - notfall: { badgeClass: 'badge-danger', label: '🔴 Notfall — sofort zum Tierarzt!' }, + beobachten: { badgeClass: 'badge-success', icon: 'check-circle', label: 'Beobachten' }, + tierarzt_heute:{ badgeClass: 'badge-warning', icon: 'warning', label: 'Heute zum Tierarzt' }, + tierarzt: { badgeClass: 'badge-warning', icon: 'warning', label: 'Zum Tierarzt' }, + tierarzt_sofort:{ badgeClass: 'badge-danger', icon: 'first-aid-kit', label: 'Sofort zum Tierarzt!' }, + notfall: { badgeClass: 'badge-danger', icon: 'first-aid-kit', label: 'Notfall — sofort zum Tierarzt!' }, }; - const d = DRINGLICHKEIT[result.dringlichkeit] || { badgeClass: 'badge-primary', label: _esc(result.dringlichkeit) }; + const d = DRINGLICHKEIT[result.dringlichkeit] || { badgeClass: 'badge-primary', icon: 'info', label: _esc(result.dringlichkeit) }; const hinweiseHtml = (result.hinweise || []).length ? `