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 += ``;
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
? `
@@ -1789,7 +1798,8 @@ window.Page_health = (() => {
resultEl.innerHTML = `
-
+
+
${d.label}
diff --git a/backend/static/js/pages/moderation.js b/backend/static/js/pages/moderation.js
index efe8ed8..3a7dc5c 100644
--- a/backend/static/js/pages/moderation.js
+++ b/backend/static/js/pages/moderation.js
@@ -267,7 +267,14 @@ window.Page_moderation = (() => {
}
function _renderUserList(el, users, total, parentEl) {
- if (!users.length) {
+ // Moderatoren (non-admins) sehen keine Admin-User — serverseitig bereits
+ // gefiltert, aber zur Sicherheit auch clientseitig nochmal ausfiltern.
+ const isAdmin = _appState?.user?.rolle === 'admin';
+ const visible = isAdmin
+ ? users
+ : users.filter(u => u.rolle !== 'admin' && !u.is_admin);
+
+ if (!visible.length) {
el.innerHTML = _emptyState('users', 'Keine Nutzer gefunden', '');
return;
}
@@ -275,7 +282,10 @@ window.Page_moderation = (() => {
${total} Nutzer gefunden
- ${users.map(u => `
+ ${visible.map(u => {
+ const isAdminUser = u.rolle === 'admin' || u.is_admin;
+ const canAction = isAdmin && !isAdminUser;
+ return `
@@ -304,22 +314,24 @@ window.Page_moderation = (() => {
- ${u.is_banned
- ? ``
- : ``
+ ${canAction
+ ? (u.is_banned
+ ? ``
+ : ``)
+ : ''
}
- `).join('')}
+ `}).join('')}
`;
diff --git a/backend/static/js/pages/notes.js b/backend/static/js/pages/notes.js
index cb23e27..c1ea9d9 100644
--- a/backend/static/js/pages/notes.js
+++ b/backend/static/js/pages/notes.js
@@ -213,6 +213,9 @@ window.Page_notes = (() => {
/* Filter-Chips */
.notes-filter-chips { display: flex; gap: var(--space-2); overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
.notes-filter-chips::-webkit-scrollbar { display: none; }
+ @media (min-width: 1024px) {
+ .notes-filter-chips { flex-wrap: wrap; overflow-x: visible; }
+ }
.notes-chip { flex-shrink: 0; font-size: var(--text-xs); font-weight: var(--weight-semibold); padding: 4px var(--space-3); border-radius: 999px; border: 1.5px solid var(--c-border); background: var(--c-surface-2); color: var(--c-text-secondary); cursor: pointer; white-space: nowrap; transition: background .15s, color .15s, border-color .15s; }
.notes-chip--active { background: var(--chip-color, var(--c-primary)); color: #fff; border-color: var(--chip-color, var(--c-primary)); }
diff --git a/backend/static/js/pages/poison.js b/backend/static/js/pages/poison.js
index dd4eb43..68e7c50 100644
--- a/backend/static/js/pages/poison.js
+++ b/backend/static/js/pages/poison.js
@@ -223,7 +223,7 @@ window.Page_poison = (() => {
if (_reports.length === 0) {
listEl.innerHTML = UI.emptyState({
- icon : 'check-circle',
+ icon : UI.icon('check-circle'),
title : 'Alles sicher',
text : 'In deiner Nähe (10 km) gibt es aktuell keine Giftköder-Meldungen.',
action: ``,
diff --git a/backend/static/sw.js b/backend/static/sw.js
index e8b12c9..b23f40a 100644
--- a/backend/static/sw.js
+++ b/backend/static/sw.js
@@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
-const CACHE_VERSION = 'by-v425';
+const CACHE_VERSION = 'by-v427';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten