Sprint 14: Multi-Fix-Batch — SW by-v428, APP_VER 407
KI/Symptom-Check: JSON-Code-Fence stripping in ki.py, Dringlichkeit-Map mit Phosphor-Icons
Gewicht-Sync: health.js aktualisiert appState.activeDog.gewicht_kg auch bei Bearbeitung
Giftköder: icon:'check-circle' → UI.icon('check-circle') in emptyState-Call
Forum-Pills: overflow:hidden + text-overflow:ellipsis auf Desktop und Mobile
Moderation: Admins für Moderatoren unsichtbar, keine Aktions-Buttons auf Admins
Notizblock: Filter-Chips wrap 2-zeilig auf Desktop (min-width:1024px)
Tagebuch: Datenschutz-Hinweis "nur du kannst sie sehen", Sitter sieht keine bestehenden Einträge
diary.py: Sitter-Zugriff gibt leere Liste zurück (GET), Erstellen bleibt erlaubt
This commit is contained in:
parent
02120bb532
commit
016eb52d83
12 changed files with 111 additions and 33 deletions
|
|
@ -220,13 +220,15 @@ Antworte NUR als JSON:
|
||||||
user_is_premium=user_is_premium,
|
user_is_premium=user_is_premium,
|
||||||
json_mode=True,
|
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:
|
try:
|
||||||
return json.loads(result)
|
return json.loads(cleaned)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return {
|
return {
|
||||||
"dringlichkeit": "tierarzt_heute",
|
"dringlichkeit": "tierarzt_heute",
|
||||||
"einschaetzung": result,
|
"einschaetzung": cleaned,
|
||||||
"hinweise": [],
|
"hinweise": [],
|
||||||
"zum_tierarzt_wenn": "Bei Verschlechterung sofort.",
|
"zum_tierarzt_wenn": "Bei Verschlechterung sofort.",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,18 @@ async def list_diary(dog_id: int, limit: int = 20, offset: int = 0,
|
||||||
user=Depends(get_current_user)):
|
user=Depends(get_current_user)):
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
_can_read_dog(dog_id, user["id"], 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 ""
|
extra = "AND (d.is_milestone=1 OR d.typ='meilenstein')" if milestone else ""
|
||||||
if q:
|
if q:
|
||||||
pattern = f"%{q}%"
|
pattern = f"%{q}%"
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,8 @@ async def mod_users(
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
user=Depends(require_moderator),
|
user=Depends(require_moderator),
|
||||||
):
|
):
|
||||||
|
is_admin = user["rolle"] == "admin"
|
||||||
|
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
where = "WHERE 1=1"
|
where = "WHERE 1=1"
|
||||||
params = []
|
params = []
|
||||||
|
|
@ -114,8 +116,12 @@ async def mod_users(
|
||||||
if banned:
|
if banned:
|
||||||
where += " AND is_banned=1"
|
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
|
# 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"
|
"SUBSTR(email,1,2)||'***@'||SUBSTR(email,INSTR(email,'@')+1) AS email"
|
||||||
|
|
||||||
rows = conn.execute(f"""
|
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:
|
with db() as conn:
|
||||||
target = conn.execute(
|
target = conn.execute(
|
||||||
"SELECT id, rolle, name FROM users WHERE id=?", (uid,)
|
"SELECT id, rolle, is_admin, name FROM users WHERE id=?", (uid,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if not target:
|
if not target:
|
||||||
raise HTTPException(404, "User nicht gefunden.")
|
raise HTTPException(404, "User nicht gefunden.")
|
||||||
if target["rolle"] == "admin" and user["rolle"] != "admin":
|
# Moderatoren dürfen keine Admins bearbeiten
|
||||||
raise HTTPException(403, "Admins können nur von Admins verwaltet werden.")
|
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)
|
cols = ", ".join(f"{k}=?" for k in updates)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
|
||||||
|
|
@ -4174,6 +4174,11 @@ html.modal-open {
|
||||||
.forum-category-tabs {
|
.forum-category-tabs {
|
||||||
padding-bottom: var(--space-1);
|
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) */
|
/* Category badge (colored pill) */
|
||||||
.forum-category-badge {
|
.forum-category-badge {
|
||||||
|
|
|
||||||
|
|
@ -653,6 +653,8 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gesundheit: Tabs auf 2 Zeilen */
|
/* Gesundheit: Tabs auf 2 Zeilen */
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
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 = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -736,6 +736,19 @@ window.Page_diary = (() => {
|
||||||
const listEl = _container.querySelector('#diary-list');
|
const listEl = _container.querySelector('#diary-list');
|
||||||
if (!listEl) return;
|
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) {
|
if (_entries.length === 0) {
|
||||||
listEl.innerHTML = UI.emptyState({
|
listEl.innerHTML = UI.emptyState({
|
||||||
icon: UI.icon('book-open'),
|
icon: UI.icon('book-open'),
|
||||||
|
|
@ -748,6 +761,16 @@ window.Page_diary = (() => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Datenschutz-Hinweis: Einträge sind privat
|
||||||
|
const privacyNotice = `
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted);background:var(--c-surface-2);
|
||||||
|
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
|
||||||
|
display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
|
||||||
|
<svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px;flex-shrink:0">
|
||||||
|
<use href="/icons/phosphor.svg#lock-simple"></use></svg>
|
||||||
|
Deine Tagebucheinträge sind privat — nur du kannst sie sehen.
|
||||||
|
</div>`;
|
||||||
|
|
||||||
// Gruppieren nach Jahr-Monat (Anzeigereihenfolge: chronologisch absteigend)
|
// Gruppieren nach Jahr-Monat (Anzeigereihenfolge: chronologisch absteigend)
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
_entries.forEach(e => {
|
_entries.forEach(e => {
|
||||||
|
|
@ -756,7 +779,7 @@ window.Page_diary = (() => {
|
||||||
groups.get(key).push(e);
|
groups.get(key).push(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
let html = '';
|
let html = privacyNotice;
|
||||||
groups.forEach((items, key) => {
|
groups.forEach((items, key) => {
|
||||||
const monthLabel = key === 'unbekannt' ? 'Datum unbekannt' : _formatMonth(key);
|
const monthLabel = key === 'unbekannt' ? 'Datum unbekannt' : _formatMonth(key);
|
||||||
html += `<div class="diary-month-header">${monthLabel}</div>`;
|
html += `<div class="diary-month-header">${monthLabel}</div>`;
|
||||||
|
|
|
||||||
|
|
@ -1209,9 +1209,16 @@ window.Page_health = (() => {
|
||||||
if (!_data[t]) _data[t] = [];
|
if (!_data[t]) _data[t] = [];
|
||||||
_data[t].unshift(saved);
|
_data[t].unshift(saved);
|
||||||
UI.toast.success('Eintrag erstellt.');
|
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
|
// Multi-File-Upload
|
||||||
|
|
@ -1767,11 +1774,13 @@ window.Page_health = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DRINGLICHKEIT = {
|
const DRINGLICHKEIT = {
|
||||||
beobachten: { badgeClass: 'badge-success', label: '🟢 Beobachten' },
|
beobachten: { badgeClass: 'badge-success', icon: 'check-circle', label: 'Beobachten' },
|
||||||
tierarzt: { badgeClass: 'badge-warning', label: '🟡 Zum Tierarzt' },
|
tierarzt_heute:{ badgeClass: 'badge-warning', icon: 'warning', label: 'Heute zum Tierarzt' },
|
||||||
notfall: { badgeClass: 'badge-danger', label: '🔴 Notfall — sofort 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
|
const hinweiseHtml = (result.hinweise || []).length
|
||||||
? `<ul style="margin:var(--space-2) 0 0;padding-left:var(--space-5);font-size:var(--text-sm)">
|
? `<ul style="margin:var(--space-2) 0 0;padding-left:var(--space-5);font-size:var(--text-sm)">
|
||||||
|
|
@ -1789,7 +1798,8 @@ window.Page_health = (() => {
|
||||||
|
|
||||||
resultEl.innerHTML = `
|
resultEl.innerHTML = `
|
||||||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
|
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
|
||||||
<span class="badge ${d.badgeClass}" style="font-size:var(--text-sm);padding:var(--space-1) var(--space-3)">
|
<span class="badge ${d.badgeClass}" style="display:inline-flex;align-items:center;gap:var(--space-1);font-size:var(--text-sm);padding:var(--space-1) var(--space-3)">
|
||||||
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${d.icon}"></use></svg>
|
||||||
${d.label}
|
${d.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -267,7 +267,14 @@ window.Page_moderation = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function _renderUserList(el, users, total, parentEl) {
|
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', '');
|
el.innerHTML = _emptyState('users', 'Keine Nutzer gefunden', '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +282,10 @@ window.Page_moderation = (() => {
|
||||||
<div style="margin-bottom:var(--space-2);font-size:var(--text-xs);
|
<div style="margin-bottom:var(--space-2);font-size:var(--text-xs);
|
||||||
color:var(--c-text-muted)">${total} Nutzer gefunden</div>
|
color:var(--c-text-muted)">${total} Nutzer gefunden</div>
|
||||||
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||||||
${users.map(u => `
|
${visible.map(u => {
|
||||||
|
const isAdminUser = u.rolle === 'admin' || u.is_admin;
|
||||||
|
const canAction = isAdmin && !isAdminUser;
|
||||||
|
return `
|
||||||
<div class="card" style="padding:var(--space-3) var(--space-4);
|
<div class="card" style="padding:var(--space-3) var(--space-4);
|
||||||
${u.is_banned ? 'opacity:0.6;border-left:3px solid var(--c-danger)' : ''}">
|
${u.is_banned ? 'opacity:0.6;border-left:3px solid var(--c-danger)' : ''}">
|
||||||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||||||
|
|
@ -304,22 +314,24 @@ window.Page_moderation = (() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex-shrink:0">
|
<div style="flex-shrink:0">
|
||||||
${u.is_banned
|
${canAction
|
||||||
? `<button class="btn btn-sm btn-ghost mod-unban"
|
? (u.is_banned
|
||||||
data-uid="${u.id}" data-name="${_esc(u.name)}"
|
? `<button class="btn btn-sm btn-ghost mod-unban"
|
||||||
title="Sperre aufheben" style="color:var(--c-success)">
|
data-uid="${u.id}" data-name="${_esc(u.name)}"
|
||||||
${UI.icon('lock-open')}
|
title="Sperre aufheben" style="color:var(--c-success)">
|
||||||
</button>`
|
${UI.icon('lock-open')}
|
||||||
: `<button class="btn btn-sm btn-ghost mod-ban"
|
</button>`
|
||||||
data-uid="${u.id}" data-name="${_esc(u.name)}"
|
: `<button class="btn btn-sm btn-ghost mod-ban"
|
||||||
title="Sperren" style="color:var(--c-danger)">
|
data-uid="${u.id}" data-name="${_esc(u.name)}"
|
||||||
${UI.icon('lock')}
|
title="Sperren" style="color:var(--c-danger)">
|
||||||
</button>`
|
${UI.icon('lock')}
|
||||||
|
</button>`)
|
||||||
|
: ''
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`}).join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,9 @@ window.Page_notes = (() => {
|
||||||
/* Filter-Chips */
|
/* Filter-Chips */
|
||||||
.notes-filter-chips { display: flex; gap: var(--space-2); overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
|
.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; }
|
.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 { 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)); }
|
.notes-chip--active { background: var(--chip-color, var(--c-primary)); color: #fff; border-color: var(--chip-color, var(--c-primary)); }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ window.Page_poison = (() => {
|
||||||
|
|
||||||
if (_reports.length === 0) {
|
if (_reports.length === 0) {
|
||||||
listEl.innerHTML = UI.emptyState({
|
listEl.innerHTML = UI.emptyState({
|
||||||
icon : 'check-circle',
|
icon : UI.icon('check-circle'),
|
||||||
title : 'Alles sicher',
|
title : 'Alles sicher',
|
||||||
text : 'In deiner Nähe (10 km) gibt es aktuell keine Giftköder-Meldungen.',
|
text : 'In deiner Nähe (10 km) gibt es aktuell keine Giftköder-Meldungen.',
|
||||||
action: `<button class="btn btn-danger" id="poison-empty-report">⚠️ Trotzdem melden</button>`,
|
action: `<button class="btn btn-danger" id="poison-empty-report">⚠️ Trotzdem melden</button>`,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v425';
|
const CACHE_VERSION = 'by-v427';
|
||||||
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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue