Fix: Ernährung Hund-spezifisch, Erinnerungen in Settings, Übung des Tages per Hund (SW by-v872)
- ernaehrung.js: onDogChange setzt activeTab zurück, Hund klar sichtbar
- settings.js: Erinnerungen-Sektion lädt verstorbene Hunde + öffnet Gedenkseite
- dogs.py: GET /dogs/verstorben Endpoint (korrekte Route-Reihenfolge vor /{dog_id})
- dogs.py: Übung des Tages filtert jetzt nach dog_id statt user_id (sitzt-Übungen korrekt ausgeschlossen)
- Routen zeigen verstorbene Hunde korrekt als Teilnehmer (route_dogs ohne verstorben-Filter)
This commit is contained in:
parent
265d3d4fe2
commit
1ce802c8dc
8 changed files with 1106 additions and 28 deletions
|
|
@ -376,7 +376,7 @@ if STAGING and os.path.isdir(PROD_MEDIA_DIR):
|
|||
else:
|
||||
app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
|
||||
|
||||
APP_VER = "856" # muss mit APP_VER in app.js übereinstimmen
|
||||
APP_VER = "872" # muss mit APP_VER in app.js übereinstimmen
|
||||
|
||||
@app.get("/.well-known/assetlinks.json")
|
||||
async def assetlinks():
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class DogUpdate(BaseModel):
|
|||
async def list_dogs(user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
own = conn.execute(
|
||||
"SELECT *, NULL AS shared_by, NULL AS share_role FROM dogs WHERE user_id=? ORDER BY id",
|
||||
"SELECT *, NULL AS shared_by, NULL AS share_role FROM dogs WHERE user_id=? AND (verstorben_am IS NULL) ORDER BY id",
|
||||
(user["id"],)
|
||||
).fetchall()
|
||||
shared = conn.execute(
|
||||
|
|
@ -255,15 +255,16 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
|
|||
day_num = (_dt.date.today() - _dt.date(2024, 1, 1)).days
|
||||
|
||||
# Versuche JOIN (funktioniert wenn js_exercise_id-Spalte vorhanden)
|
||||
# Nur Übungen des aktiven Hundes, 'sitzt' ausschließen
|
||||
try:
|
||||
joined = conn.execute(
|
||||
"""SELECT ep.exercise_id, te.name, te.kategorie AS kategorie_raw,
|
||||
te.schwierigkeit, te.js_exercise_id
|
||||
FROM exercise_progress ep
|
||||
JOIN training_exercises te ON te.js_exercise_id = ep.exercise_id
|
||||
WHERE ep.user_id = ? AND ep.status IN ('noch-nicht', 'manchmal', 'meistens')
|
||||
WHERE ep.dog_id = ? AND ep.status IN ('noch-nicht', 'manchmal', 'meistens')
|
||||
ORDER BY ep.updated_at ASC LIMIT 50""",
|
||||
(user["id"],)
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
except Exception:
|
||||
joined = []
|
||||
|
|
@ -288,9 +289,9 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
|
|||
)
|
||||
raw = conn.execute(
|
||||
"""SELECT exercise_id FROM exercise_progress
|
||||
WHERE user_id = ? AND status IN ('noch-nicht', 'manchmal', 'meistens')
|
||||
WHERE dog_id = ? AND status IN ('noch-nicht', 'manchmal', 'meistens')
|
||||
ORDER BY updated_at ASC LIMIT 50""",
|
||||
(user["id"],)
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
valid = [r["exercise_id"] for r in raw
|
||||
if any(r["exercise_id"].startswith(p) for p in _KNOWN_PREFIXES)]
|
||||
|
|
@ -779,6 +780,21 @@ async def get_hunde_buch(
|
|||
return HTMLResponse(content=html_page)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/dogs/verstorben — Alle verstorbenen Hunde des Users
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/verstorben")
|
||||
async def get_verstorbene_hunde(user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
rows = conn.execute(
|
||||
"""SELECT id, name, rasse, foto_url, verstorben_am, geburtstag
|
||||
FROM dogs WHERE user_id=? AND verstorben_am IS NOT NULL
|
||||
ORDER BY verstorben_am DESC""",
|
||||
(user["id"],)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/{dog_id}")
|
||||
async def get_dog(dog_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
|
|
@ -1208,14 +1224,15 @@ async def get_dog_timeline(dog_id: int, user=Depends(get_current_user)):
|
|||
"ref_id": r["id"],
|
||||
})
|
||||
|
||||
# --- Routen ---
|
||||
# --- Routen (nur Routen wo dieser Hund mitgegangen ist) ---
|
||||
route_rows = conn.execute(
|
||||
"""SELECT id, name, distanz_km,
|
||||
date(created_at) AS datum
|
||||
FROM routes
|
||||
WHERE user_id=?
|
||||
ORDER BY created_at ASC""",
|
||||
(user["id"],)
|
||||
"""SELECT r.id, r.name, r.distanz_km,
|
||||
date(r.created_at) AS datum
|
||||
FROM routes r
|
||||
JOIN route_dogs rd ON rd.route_id = r.id AND rd.dog_id = ?
|
||||
WHERE r.user_id = ?
|
||||
ORDER BY r.created_at ASC""",
|
||||
(dog_id, user["id"])
|
||||
).fetchall()
|
||||
|
||||
route_first = True
|
||||
|
|
@ -1263,3 +1280,108 @@ async def get_dog_timeline(dog_id: int, user=Depends(get_current_user)):
|
|||
"geburtstag": dog["geburtstag"],
|
||||
"events": events,
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/dogs/{id}/gedenken — Hund als verstorben markieren
|
||||
# ------------------------------------------------------------------
|
||||
class GedenkenData(BaseModel):
|
||||
verstorben_am: str # YYYY-MM-DD
|
||||
|
||||
@router.post("/{dog_id}/gedenken")
|
||||
async def mark_verstorben(dog_id: int, data: GedenkenData, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
updated = conn.execute(
|
||||
"UPDATE dogs SET verstorben_am=? WHERE id=? AND user_id=?",
|
||||
(data.verstorben_am, dog_id, user["id"])
|
||||
).rowcount
|
||||
if not updated:
|
||||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/dogs/{id}/gedenkseite — Memorial-Daten
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/{dog_id}/gedenkseite")
|
||||
async def get_gedenkseite(dog_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
dog = conn.execute(
|
||||
"SELECT * FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"])
|
||||
).fetchone()
|
||||
if not dog:
|
||||
raise HTTPException(404)
|
||||
dog = dict(dog)
|
||||
|
||||
# Statistiken
|
||||
km_total = conn.execute(
|
||||
"SELECT COALESCE(ROUND(SUM(distanz_km),1),0) AS km FROM routes r "
|
||||
"JOIN route_dogs rd ON rd.route_id=r.id WHERE rd.dog_id=?", (dog_id,)
|
||||
).fetchone()["km"]
|
||||
|
||||
diary_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM diary WHERE dog_id=?", (dog_id,)
|
||||
).fetchone()[0]
|
||||
|
||||
media_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM diary_media dm JOIN diary d ON d.id=dm.diary_id "
|
||||
"WHERE d.dog_id=? AND dm.media_type='image'", (dog_id,)
|
||||
).fetchone()[0]
|
||||
|
||||
training_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM training_sessions WHERE dog_id=?", (dog_id,)
|
||||
).fetchone()[0]
|
||||
|
||||
# Letzter Tagebucheintrag
|
||||
last_entry = conn.execute(
|
||||
"SELECT titel, datum FROM diary WHERE dog_id=? ORDER BY datum DESC LIMIT 1",
|
||||
(dog_id,)
|
||||
).fetchone()
|
||||
|
||||
# Erste und letzte Aufnahme
|
||||
first_entry = conn.execute(
|
||||
"SELECT datum FROM diary WHERE dog_id=? ORDER BY datum ASC LIMIT 1",
|
||||
(dog_id,)
|
||||
).fetchone()
|
||||
|
||||
# Letzte 6 Fotos für Galerie
|
||||
photos = conn.execute(
|
||||
"""SELECT dm.url FROM diary_media dm
|
||||
JOIN diary d ON d.id=dm.diary_id
|
||||
WHERE d.dog_id=? AND dm.media_type='image'
|
||||
AND dm.img_width IS NOT NULL AND dm.img_width > dm.img_height
|
||||
ORDER BY d.datum DESC, dm.id DESC LIMIT 6""",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
if not photos:
|
||||
photos = conn.execute(
|
||||
"""SELECT dm.url FROM diary_media dm
|
||||
JOIN diary d ON d.id=dm.diary_id
|
||||
WHERE d.dog_id=? AND dm.media_type='image'
|
||||
ORDER BY d.datum DESC, dm.id DESC LIMIT 6""",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
|
||||
# Gemeinsame Zeit berechnen
|
||||
joined = dog.get("geburtstag") or (first_entry["datum"] if first_entry else None)
|
||||
passed = dog.get("verstorben_am")
|
||||
gemeinsam_tage = None
|
||||
if joined and passed:
|
||||
try:
|
||||
from datetime import date as _date
|
||||
d1 = _date.fromisoformat(joined)
|
||||
d2 = _date.fromisoformat(passed)
|
||||
gemeinsam_tage = (d2 - d1).days
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"dog": dog,
|
||||
"km_total": km_total,
|
||||
"diary_count": diary_count,
|
||||
"media_count": media_count,
|
||||
"training_count": training_count,
|
||||
"last_entry": dict(last_entry) if last_entry else None,
|
||||
"gemeinsam_tage": gemeinsam_tage,
|
||||
"photos": [r["url"] for r in photos],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,3 +143,305 @@ async def ki_ernaehrung(dog_id: int, body: KiBeratungRequest,
|
|||
raise HTTPException(503, str(e))
|
||||
except Exception:
|
||||
raise HTTPException(500, "KI momentan nicht verfügbar.")
|
||||
|
||||
|
||||
# ==================================================================
|
||||
# FUTTER-VERTRÄGLICHKEIT
|
||||
# ==================================================================
|
||||
|
||||
REAKTION_TYPEN = {
|
||||
# Positiv
|
||||
"verdauung_gut": {"label": "Gute Verdauung", "kategorie": "positiv", "fenster_h": 8},
|
||||
"energie_hoch": {"label": "Viel Energie", "kategorie": "positiv", "fenster_h": 12},
|
||||
"fell_glaenzend": {"label": "Glänzendes Fell", "kategorie": "positiv", "fenster_h": 336}, # 2 Wochen
|
||||
# Gastrointestinal
|
||||
"erbrechen": {"label": "Erbrechen", "kategorie": "gastro_negativ", "fenster_h": 6},
|
||||
"durchfall": {"label": "Durchfall", "kategorie": "gastro_negativ", "fenster_h": 8},
|
||||
"blaehungen": {"label": "Blähungen", "kategorie": "gastro_negativ", "fenster_h": 6},
|
||||
"weicher_stuhl": {"label": "Weicher Stuhl", "kategorie": "gastro_negativ", "fenster_h": 8},
|
||||
"appetitlosigkeit":{"label": "Appetitlosigkeit", "kategorie": "gastro_negativ", "fenster_h": 12},
|
||||
# Haut & Fell
|
||||
"juckreiz": {"label": "Juckreiz / Kratzen", "kategorie": "haut_negativ", "fenster_h": 72},
|
||||
"haarausfall": {"label": "Haarausfall", "kategorie": "haut_negativ", "fenster_h": 336},
|
||||
"stumpfes_fell": {"label": "Stumpfes Fell", "kategorie": "haut_negativ", "fenster_h": 336},
|
||||
"schuppenbildung":{"label": "Schuppenbildung", "kategorie": "haut_negativ", "fenster_h": 168},
|
||||
"roetungen": {"label": "Hautrötungen / Entzündung", "kategorie": "haut_negativ", "fenster_h": 72},
|
||||
"pfotenlecken": {"label": "Pfoten lecken (chronisch)", "kategorie": "haut_negativ", "fenster_h": 168},
|
||||
"ohrentzuendung": {"label": "Ohrentzündung", "kategorie": "haut_negativ", "fenster_h": 168},
|
||||
"fettiges_fell": {"label": "Fettiges Fell / Seborrhö", "kategorie": "haut_negativ", "fenster_h": 336},
|
||||
# Allgemeinbefinden
|
||||
"schlappheit": {"label": "Schlappheit / Apathie", "kategorie": "allgemein_negativ", "fenster_h": 12},
|
||||
"nervositaet": {"label": "Nervosität / Unruhe", "kategorie": "allgemein_negativ", "fenster_h": 12},
|
||||
"viel_trinken": {"label": "Ungewöhnlich viel trinken", "kategorie": "allgemein_negativ", "fenster_h": 24},
|
||||
"sonstiges": {"label": "Sonstiges", "kategorie": "sonstiges", "fenster_h": 24},
|
||||
}
|
||||
|
||||
_POSITIV_KAT = {"positiv"}
|
||||
_NEGATIV_KAT = {"gastro_negativ", "haut_negativ", "allgemein_negativ"}
|
||||
_HAUT_HINWEIS = "Haut- & Fell-Symptome wie {label} entwickeln sich typischerweise über Wochen. Mindestens 4–6 Wochen Beobachtung empfohlen — auch nach einem Futterwechsel dauert eine Besserung 2–6 Wochen."
|
||||
_GASTRO_HINWEIS = "Magen-Darm-Symptome wie {label} treten meist innerhalb weniger Stunden auf. Wenn sie häufig wiederkehren, ist ein Tierarztbesuch empfohlen."
|
||||
|
||||
|
||||
class FutterEintragCreate(BaseModel):
|
||||
datum: str
|
||||
uhrzeit: str
|
||||
futter_name: str
|
||||
futter_typ: Optional[str] = "trockenfutter"
|
||||
menge_g: Optional[int] = None
|
||||
notiz: Optional[str] = None
|
||||
|
||||
|
||||
class ReaktionCreate(BaseModel):
|
||||
datum: str
|
||||
uhrzeit: str
|
||||
reaktion_typ: str
|
||||
intensitaet: Optional[int] = 3
|
||||
notiz: Optional[str] = None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /dogs/{dog_id}/futter
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/{dog_id}/futter")
|
||||
async def create_futter_eintrag(dog_id: int, body: FutterEintragCreate,
|
||||
user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_check_dog_access(conn, dog_id, user["id"])
|
||||
cur = conn.execute("""
|
||||
INSERT INTO futter_eintraege
|
||||
(dog_id, datum, uhrzeit, futter_name, futter_typ, menge_g, notiz)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (dog_id, body.datum, body.uhrzeit, body.futter_name,
|
||||
body.futter_typ or "trockenfutter", body.menge_g, body.notiz))
|
||||
row = conn.execute(
|
||||
"SELECT * FROM futter_eintraege WHERE id=?", (cur.lastrowid,)
|
||||
).fetchone()
|
||||
return dict(row)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /dogs/{dog_id}/futter
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/{dog_id}/futter")
|
||||
async def list_futter_eintraege(dog_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_check_dog_access(conn, dog_id, user["id"])
|
||||
rows = conn.execute("""
|
||||
SELECT * FROM futter_eintraege
|
||||
WHERE dog_id=?
|
||||
ORDER BY datum DESC, uhrzeit DESC
|
||||
LIMIT 50
|
||||
""", (dog_id,)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /dogs/{dog_id}/futter/{entry_id}
|
||||
# ------------------------------------------------------------------
|
||||
@router.delete("/{dog_id}/futter/{entry_id}")
|
||||
async def delete_futter_eintrag(dog_id: int, entry_id: int,
|
||||
user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_check_dog_access(conn, dog_id, user["id"])
|
||||
result = conn.execute(
|
||||
"DELETE FROM futter_eintraege WHERE id=? AND dog_id=?",
|
||||
(entry_id, dog_id)
|
||||
)
|
||||
if result.rowcount == 0:
|
||||
raise HTTPException(404, "Eintrag nicht gefunden.")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /dogs/{dog_id}/futter/reaktion
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/{dog_id}/futter/reaktion")
|
||||
async def create_reaktion(dog_id: int, body: ReaktionCreate,
|
||||
user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_check_dog_access(conn, dog_id, user["id"])
|
||||
cur = conn.execute("""
|
||||
INSERT INTO futter_reaktionen
|
||||
(dog_id, datum, uhrzeit, reaktion_typ, intensitaet, notiz)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (dog_id, body.datum, body.uhrzeit, body.reaktion_typ,
|
||||
body.intensitaet or 3, body.notiz))
|
||||
row = conn.execute(
|
||||
"SELECT * FROM futter_reaktionen WHERE id=?", (cur.lastrowid,)
|
||||
).fetchone()
|
||||
return dict(row)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /dogs/{dog_id}/futter/reaktionen
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/{dog_id}/futter/reaktionen")
|
||||
async def list_reaktionen(dog_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_check_dog_access(conn, dog_id, user["id"])
|
||||
rows = conn.execute("""
|
||||
SELECT * FROM futter_reaktionen
|
||||
WHERE dog_id=?
|
||||
ORDER BY datum DESC, uhrzeit DESC
|
||||
LIMIT 50
|
||||
""", (dog_id,)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /dogs/{dog_id}/futter/reaktion/{react_id}
|
||||
# ------------------------------------------------------------------
|
||||
@router.delete("/{dog_id}/futter/reaktion/{react_id}")
|
||||
async def delete_reaktion(dog_id: int, react_id: int,
|
||||
user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_check_dog_access(conn, dog_id, user["id"])
|
||||
result = conn.execute(
|
||||
"DELETE FROM futter_reaktionen WHERE id=? AND dog_id=?",
|
||||
(react_id, dog_id)
|
||||
)
|
||||
if result.rowcount == 0:
|
||||
raise HTTPException(404, "Reaktion nicht gefunden.")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /dogs/{dog_id}/futter/analyse
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/{dog_id}/futter/analyse")
|
||||
async def futter_analyse(dog_id: int, user=Depends(get_current_user)):
|
||||
from datetime import datetime
|
||||
|
||||
with db() as conn:
|
||||
_check_dog_access(conn, dog_id, user["id"])
|
||||
|
||||
eintraege = conn.execute(
|
||||
"SELECT * FROM futter_eintraege WHERE dog_id=? ORDER BY datum, uhrzeit",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
reaktionen = conn.execute(
|
||||
"SELECT * FROM futter_reaktionen WHERE dog_id=? ORDER BY datum, uhrzeit",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
|
||||
def parse_ts(datum, uhrzeit):
|
||||
try:
|
||||
return datetime.fromisoformat(f"{datum}T{uhrzeit}")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# futter_name → {typ, mahlzeiten, positiv, negativ, kategorien: {kat: count}}
|
||||
futter_stats: dict = {}
|
||||
|
||||
for e in eintraege:
|
||||
name = e["futter_name"]
|
||||
if name not in futter_stats:
|
||||
futter_stats[name] = {
|
||||
"name": name,
|
||||
"typ": e["futter_typ"],
|
||||
"mahlzeiten": 0,
|
||||
"positiv": 0,
|
||||
"negativ": 0,
|
||||
"kategorien": {},
|
||||
}
|
||||
futter_stats[name]["mahlzeiten"] += 1
|
||||
|
||||
for r in reaktionen:
|
||||
r_ts = parse_ts(r["datum"], r["uhrzeit"])
|
||||
if not r_ts:
|
||||
continue
|
||||
r_typ = r["reaktion_typ"]
|
||||
meta = REAKTION_TYPEN.get(r_typ, {"kategorie": "sonstiges", "fenster_h": 24})
|
||||
kat = meta["kategorie"]
|
||||
fenster = meta["fenster_h"]
|
||||
# Mindestfenster 1h, maximales Fenster wie angegeben
|
||||
min_h = 1
|
||||
|
||||
for e in eintraege:
|
||||
e_ts = parse_ts(e["datum"], e["uhrzeit"])
|
||||
if not e_ts:
|
||||
continue
|
||||
diff = (r_ts - e_ts).total_seconds() / 3600
|
||||
if min_h <= diff <= fenster:
|
||||
name = e["futter_name"]
|
||||
if name not in futter_stats:
|
||||
continue
|
||||
if kat in _POSITIV_KAT:
|
||||
futter_stats[name]["positiv"] += 1
|
||||
elif kat in _NEGATIV_KAT:
|
||||
futter_stats[name]["negativ"] += 1
|
||||
# Kategorie-Zähler
|
||||
futter_stats[name]["kategorien"][kat] = \
|
||||
futter_stats[name]["kategorien"].get(kat, 0) + 1
|
||||
|
||||
result_futter = []
|
||||
for stats in futter_stats.values():
|
||||
positiv = stats["positiv"]
|
||||
negativ = stats["negativ"]
|
||||
total = positiv + negativ
|
||||
if total == 0:
|
||||
score = 50
|
||||
status = "neu"
|
||||
else:
|
||||
raw = (positiv - negativ * 2) / max(1, total)
|
||||
# raw liegt zwischen -2 und 1 → normieren auf 0-100
|
||||
score = int(max(0, min(100, (raw + 2) / 3 * 100)))
|
||||
if score >= 60:
|
||||
status = "gut"
|
||||
elif score >= 30:
|
||||
status = "neutral"
|
||||
else:
|
||||
status = "problematisch"
|
||||
|
||||
result_futter.append({
|
||||
"name": stats["name"],
|
||||
"typ": stats["typ"],
|
||||
"mahlzeiten": stats["mahlzeiten"],
|
||||
"positiv": positiv,
|
||||
"negativ": negativ,
|
||||
"score": score,
|
||||
"status": status,
|
||||
"kategorien": stats["kategorien"],
|
||||
})
|
||||
|
||||
# Sortierung: problematisch → neutral → gut → neu, dann nach Score
|
||||
ORDER = {"problematisch": 0, "neutral": 1, "gut": 2, "neu": 3}
|
||||
result_futter.sort(key=lambda x: (ORDER.get(x["status"], 9), -x["score"]))
|
||||
|
||||
# Hinweis ableiten: erstes problematisches Futter mit Haut/Gastro-Symptomen
|
||||
hinweis = None
|
||||
for f in result_futter:
|
||||
if f["status"] != "problematisch":
|
||||
continue
|
||||
kats = f["kategorien"]
|
||||
if kats.get("haut_negativ", 0) > 0:
|
||||
# Häufigstes Haut-Symptom finden
|
||||
haut_rxn = [
|
||||
r["reaktion_typ"] for r in reaktionen
|
||||
if REAKTION_TYPEN.get(r["reaktion_typ"], {}).get("kategorie") == "haut_negativ"
|
||||
]
|
||||
label = REAKTION_TYPEN.get(haut_rxn[0], {}).get("label", "Haut-Symptome") if haut_rxn else "Haut-Symptome"
|
||||
hinweis = _HAUT_HINWEIS.format(label=label)
|
||||
break
|
||||
if kats.get("gastro_negativ", 0) > 0:
|
||||
gastro_rxn = [
|
||||
r["reaktion_typ"] for r in reaktionen
|
||||
if REAKTION_TYPEN.get(r["reaktion_typ"], {}).get("kategorie") == "gastro_negativ"
|
||||
]
|
||||
label = REAKTION_TYPEN.get(gastro_rxn[0], {}).get("label", "Magen-Darm-Symptome") if gastro_rxn else "Magen-Darm-Symptome"
|
||||
hinweis = _GASTRO_HINWEIS.format(label=label)
|
||||
break
|
||||
|
||||
# Kategorien-Übersicht über alle Reaktionen
|
||||
kategorien_gesamt: dict = {}
|
||||
for r in reaktionen:
|
||||
kat = REAKTION_TYPEN.get(r["reaktion_typ"], {}).get("kategorie", "sonstiges")
|
||||
kategorien_gesamt[kat] = kategorien_gesamt.get(kat, 0) + 1
|
||||
|
||||
return {
|
||||
"eintraege_count": len(eintraege),
|
||||
"reaktionen_count": len(reaktionen),
|
||||
"futter": result_futter,
|
||||
"kategorien": kategorien_gesamt,
|
||||
"hinweis": hinweis,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,9 +101,9 @@
|
|||
</script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=856">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=856">
|
||||
<link rel="stylesheet" href="/css/components.css?v=856">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=872">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=872">
|
||||
<link rel="stylesheet" href="/css/components.css?v=872">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -583,10 +583,10 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=856"></script>
|
||||
<script src="/js/ui.js?v=856"></script>
|
||||
<script src="/js/app.js?v=856"></script>
|
||||
<script src="/js/worlds.js?v=856"></script>
|
||||
<script src="/js/api.js?v=872"></script>
|
||||
<script src="/js/ui.js?v=872"></script>
|
||||
<script src="/js/app.js?v=872"></script>
|
||||
<script src="/js/worlds.js?v=872"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '856'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '872'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
|
||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
||||
|
|
@ -905,6 +905,12 @@ const App = (() => {
|
|||
if (!dog || dog.id === state.activeDog?.id) return;
|
||||
state.activeDog = dog;
|
||||
localStorage.setItem('by_active_dog', String(dogId));
|
||||
// SW-Cache für hund-spezifische Daten invalidieren
|
||||
navigator.serviceWorker?.controller?.postMessage({
|
||||
type: 'INVALIDATE_CACHE',
|
||||
paths: ['/api/training/progress', '/api/training/plan-progress',
|
||||
'/api/training/suggestions', `/api/dogs/${dogId}/welcome-dashboard`],
|
||||
});
|
||||
_renderDogSwitcher();
|
||||
_notifyDogChange();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ window.Page_ernaehrung = (() => {
|
|||
{ key: 'guide', label: 'Futter-Guide', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>' },
|
||||
{ key: 'gift', label: 'Giftliste', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>' },
|
||||
{ key: 'ki', label: 'KI-Berater', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#robot"></use></svg>' },
|
||||
{ key: 'vertraeglichkeit', label: 'Verträglichkeit', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#heartbeat"></use></svg>' },
|
||||
];
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -47,6 +48,7 @@ window.Page_ernaehrung = (() => {
|
|||
|
||||
async function onDogChange() {
|
||||
_profil = {};
|
||||
_activeTab = 'rechner'; // Tab zurücksetzen damit neuer Hund frisch startet
|
||||
await _render();
|
||||
}
|
||||
|
||||
|
|
@ -73,10 +75,12 @@ window.Page_ernaehrung = (() => {
|
|||
}
|
||||
|
||||
_container.innerHTML = `
|
||||
<div style="padding:var(--space-3) var(--space-4) 0">${UI.dogChip(_appState)}</div>
|
||||
<div class="by-tabs" id="ern-tabs"></div>
|
||||
<div id="ern-tab-content"></div>
|
||||
`;
|
||||
|
||||
UI.bindDogChip(_container, _appState);
|
||||
_renderTabBar();
|
||||
_renderTab();
|
||||
}
|
||||
|
|
@ -106,10 +110,11 @@ window.Page_ernaehrung = (() => {
|
|||
const el = _container.querySelector('#ern-tab-content');
|
||||
if (!el) return;
|
||||
switch (_activeTab) {
|
||||
case 'rechner': _renderRechner(el); break;
|
||||
case 'guide': _renderGuide(el); break;
|
||||
case 'gift': _renderGift(el); break;
|
||||
case 'ki': _renderKi(el); break;
|
||||
case 'rechner': _renderRechner(el); break;
|
||||
case 'guide': _renderGuide(el); break;
|
||||
case 'gift': _renderGift(el); break;
|
||||
case 'ki': _renderKi(el); break;
|
||||
case 'vertraeglichkeit': _renderVertraeglichkeit(el); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -630,6 +635,531 @@ window.Page_ernaehrung = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB 5: VERTRÄGLICHKEIT
|
||||
// ------------------------------------------------------------------
|
||||
async function _renderVertraeglichkeit(el) {
|
||||
const dog = _appState?.activeDog;
|
||||
if (!dog) { el.innerHTML = ''; return; }
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="padding:var(--space-4) 0">
|
||||
|
||||
<!-- Schnell-Erfassung -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin-bottom:var(--space-4)">
|
||||
<button class="btn btn-primary" id="vert-btn-futter">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bowl-food"></use></svg>
|
||||
Futter erfassen
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="vert-btn-reaktion">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#heartbeat"></use></svg>
|
||||
Reaktion erfassen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Info-Banner Haut/Fell -->
|
||||
<div style="display:flex;align-items:flex-start;gap:var(--space-2);
|
||||
background:var(--c-surface);border:1px solid var(--c-border);
|
||||
border-radius:var(--radius-md);padding:var(--space-3);
|
||||
margin-bottom:var(--space-4);font-size:var(--text-xs);
|
||||
color:var(--c-text-secondary);line-height:1.5">
|
||||
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-primary);margin-top:1px">
|
||||
<use href="/icons/phosphor.svg#info"></use>
|
||||
</svg>
|
||||
<span>Haut- & Fellsymptome zeigen sich erst nach Wochen — trage regelmäßig ein um Muster zu erkennen.</span>
|
||||
</div>
|
||||
|
||||
<!-- Analyse -->
|
||||
<div id="vert-analyse" style="margin-bottom:var(--space-5)">
|
||||
<div style="text-align:center;color:var(--c-text-muted);padding:var(--space-4)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#circle-notch"></use></svg>
|
||||
Lade Analyse…
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verlauf -->
|
||||
<div id="vert-verlauf"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.querySelector('#vert-btn-futter').addEventListener('click', () => _openFutterModal(el, dog));
|
||||
el.querySelector('#vert-btn-reaktion').addEventListener('click', () => _openReaktionModal(el, dog));
|
||||
|
||||
await _loadAnalyse(el, dog);
|
||||
await _loadVerlauf(el, dog);
|
||||
}
|
||||
|
||||
function _todayStr() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
function _nowTimeStr() {
|
||||
const d = new Date();
|
||||
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function _openFutterModal(el, dog) {
|
||||
const id = `fm-${Date.now()}`;
|
||||
const body = `
|
||||
<form id="${id}">
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Datum</label>
|
||||
<input type="date" name="datum" class="form-control by-input" value="${_todayStr()}" required>
|
||||
</div>
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Uhrzeit</label>
|
||||
<input type="time" name="uhrzeit" class="form-control by-input" value="${_nowTimeStr()}" required>
|
||||
</div>
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Futter-Name</label>
|
||||
<input type="text" name="futter_name" class="form-control by-input"
|
||||
list="vert-futter-datalist" placeholder="z. B. Wolfsblut Adult" required>
|
||||
<datalist id="vert-futter-datalist"></datalist>
|
||||
</div>
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Futter-Typ</label>
|
||||
<select name="futter_typ" class="form-control by-select">
|
||||
<option value="trockenfutter">Trockenfutter</option>
|
||||
<option value="nassfutter">Nassfutter</option>
|
||||
<option value="barf">BARF</option>
|
||||
<option value="snack">Snack</option>
|
||||
<option value="sonstiges">Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Menge (g, optional)</label>
|
||||
<input type="number" name="menge_g" class="form-control by-input" min="1" placeholder="">
|
||||
</div>
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Notiz (optional)</label>
|
||||
<textarea name="notiz" class="form-control by-input" rows="2" placeholder=""></textarea>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
const footer = `
|
||||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||||
<button class="btn btn-primary" id="vert-futter-save-btn" form="${id}">Speichern</button>
|
||||
`;
|
||||
UI.modal.open({ title: 'Futter erfassen', body, footer });
|
||||
|
||||
// Datalist mit bekannten Futter-Namen füllen
|
||||
API.dogs.futterList(dog.id).then(list => {
|
||||
const dl = document.getElementById('vert-futter-datalist');
|
||||
if (!dl) return;
|
||||
const names = [...new Set((list || []).map(e => e.futter_name))];
|
||||
dl.innerHTML = names.map(n => `<option value="${_esc(n)}">`).join('');
|
||||
}).catch(() => {});
|
||||
|
||||
setTimeout(() => {
|
||||
const saveBtn = document.getElementById('vert-futter-save-btn');
|
||||
if (!saveBtn) return;
|
||||
saveBtn.addEventListener('click', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const form = document.getElementById(id);
|
||||
if (!form) return;
|
||||
const fd = new FormData(form);
|
||||
const data = {
|
||||
datum: fd.get('datum'),
|
||||
uhrzeit: fd.get('uhrzeit'),
|
||||
futter_name: (fd.get('futter_name') || '').trim(),
|
||||
futter_typ: fd.get('futter_typ') || 'trockenfutter',
|
||||
menge_g: fd.get('menge_g') ? parseInt(fd.get('menge_g')) : null,
|
||||
notiz: (fd.get('notiz') || '').trim() || null,
|
||||
};
|
||||
if (!data.futter_name) { UI.toast.warning('Bitte Futter-Name angeben.'); return; }
|
||||
await UI.asyncButton(saveBtn, async () => {
|
||||
try {
|
||||
await API.dogs.futterCreate(dog.id, data);
|
||||
UI.modal.close();
|
||||
UI.toast.success('Futter gespeichert.');
|
||||
const tabEl = _container.querySelector('#ern-tab-content');
|
||||
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function _openReaktionModal(el, dog) {
|
||||
const id = `rm-${Date.now()}`;
|
||||
const body = `
|
||||
<form id="${id}">
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Datum</label>
|
||||
<input type="date" name="datum" class="form-control by-input" value="${_todayStr()}" required>
|
||||
</div>
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Uhrzeit</label>
|
||||
<input type="time" name="uhrzeit" class="form-control by-input" value="${_nowTimeStr()}" required>
|
||||
</div>
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Reaktion</label>
|
||||
<select name="reaktion_typ" class="form-control by-select" required>
|
||||
<optgroup label="✓ Positiv">
|
||||
<option value="fell_glaenzend">Glänzendes Fell</option>
|
||||
<option value="verdauung_gut">Gute Verdauung</option>
|
||||
<option value="energie_hoch">Viel Energie</option>
|
||||
</optgroup>
|
||||
<optgroup label="Magen & Darm">
|
||||
<option value="erbrechen">Erbrechen</option>
|
||||
<option value="durchfall">Durchfall</option>
|
||||
<option value="blaehungen">Blähungen</option>
|
||||
<option value="weicher_stuhl">Weicher Stuhl</option>
|
||||
<option value="appetitlosigkeit">Appetitlosigkeit</option>
|
||||
</optgroup>
|
||||
<optgroup label="Haut & Fell">
|
||||
<option value="juckreiz">Juckreiz / Kratzen</option>
|
||||
<option value="haarausfall">Haarausfall</option>
|
||||
<option value="stumpfes_fell">Stumpfes Fell</option>
|
||||
<option value="schuppenbildung">Schuppenbildung</option>
|
||||
<option value="roetungen">Hautrötungen / Entzündung</option>
|
||||
<option value="pfotenlecken">Pfoten lecken (chronisch)</option>
|
||||
<option value="ohrentzuendung">Ohrentzündung</option>
|
||||
<option value="fettiges_fell">Fettiges Fell / Seborrhö</option>
|
||||
</optgroup>
|
||||
<optgroup label="Allgemeinbefinden">
|
||||
<option value="schlappheit">Schlappheit / Apathie</option>
|
||||
<option value="nervositaet">Nervosität / Unruhe</option>
|
||||
<option value="viel_trinken">Ungewöhnlich viel trinken</option>
|
||||
<option value="sonstiges">Sonstiges</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Intensität (1–5)</label>
|
||||
<div class="vert-stern-gruppe" style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
${[1,2,3,4,5].map(n => `
|
||||
<button type="button" class="vert-stern${n <= 3 ? ' active' : ''}"
|
||||
data-val="${n}"
|
||||
style="width:40px;height:40px;border-radius:8px;
|
||||
border:1.5px solid var(--c-border);
|
||||
background:${n <= 3 ? 'var(--c-primary)' : 'var(--c-bg-card)'};
|
||||
color:${n <= 3 ? '#fff' : 'var(--c-text-secondary)'};
|
||||
font-weight:700;cursor:pointer;">${n}</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
<input type="hidden" name="intensitaet" value="3">
|
||||
</div>
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Notiz (optional)</label>
|
||||
<textarea name="notiz" class="form-control by-input" rows="2" placeholder=""></textarea>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
const footer = `
|
||||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||||
<button class="btn btn-primary" id="vert-reaktion-save-btn" form="${id}">Speichern</button>
|
||||
`;
|
||||
UI.modal.open({ title: 'Reaktion erfassen', body, footer });
|
||||
|
||||
setTimeout(() => {
|
||||
// Stern-Buttons
|
||||
document.querySelectorAll('.vert-stern').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const val = parseInt(btn.dataset.val);
|
||||
const form = document.getElementById(id);
|
||||
if (form) form.querySelector('[name=intensitaet]').value = val;
|
||||
document.querySelectorAll('.vert-stern').forEach(b => {
|
||||
const v = parseInt(b.dataset.val);
|
||||
b.style.background = v <= val ? 'var(--c-primary)' : 'var(--c-bg-card)';
|
||||
b.style.color = v <= val ? '#fff' : 'var(--c-text-secondary)';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const saveBtn = document.getElementById('vert-reaktion-save-btn');
|
||||
if (!saveBtn) return;
|
||||
saveBtn.addEventListener('click', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const form = document.getElementById(id);
|
||||
if (!form) return;
|
||||
const fd = new FormData(form);
|
||||
const data = {
|
||||
datum: fd.get('datum'),
|
||||
uhrzeit: fd.get('uhrzeit'),
|
||||
reaktion_typ: fd.get('reaktion_typ'),
|
||||
intensitaet: parseInt(fd.get('intensitaet')) || 3,
|
||||
notiz: (fd.get('notiz') || '').trim() || null,
|
||||
};
|
||||
await UI.asyncButton(saveBtn, async () => {
|
||||
try {
|
||||
await API.dogs.reaktionCreate(dog.id, data);
|
||||
UI.modal.close();
|
||||
UI.toast.success('Reaktion gespeichert.');
|
||||
const tabEl = _container.querySelector('#ern-tab-content');
|
||||
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
|
||||
async function _loadAnalyse(el, dog) {
|
||||
const analyseEl = el.querySelector('#vert-analyse');
|
||||
if (!analyseEl) return;
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await API.dogs.futterAnalyse(dog.id);
|
||||
} catch (_) {
|
||||
analyseEl.innerHTML = `<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Analyse nicht verfügbar.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.futter || data.futter.length === 0) {
|
||||
analyseEl.innerHTML = `
|
||||
<div style="text-align:center;padding:var(--space-4);color:var(--c-text-muted);font-size:var(--text-sm)">
|
||||
<svg class="ph-icon" style="width:2rem;height:2rem;margin-bottom:8px;display:block;margin-inline:auto" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#chart-bar"></use>
|
||||
</svg>
|
||||
Noch keine Einträge. Erfasse Futter und Reaktionen um die Analyse zu sehen.
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const STATUS_CFG = {
|
||||
gut: { label: 'Gut verträglich', color: 'var(--c-success,#22c55e)', bg: 'rgba(34,197,94,0.12)' },
|
||||
neutral: { label: 'Neutral', color: 'var(--c-warning,#f59e0b)', bg: 'rgba(245,158,11,0.12)' },
|
||||
problematisch:{ label: 'Problematisch', color: 'var(--c-danger,#ef4444)', bg: 'rgba(239,68,68,0.12)' },
|
||||
neu: { label: 'Zu wenig Daten', color: 'var(--c-text-muted)', bg: 'var(--c-surface)' },
|
||||
};
|
||||
|
||||
const TYP_LABELS = {
|
||||
trockenfutter: 'Trockenfutter', nassfutter: 'Nassfutter',
|
||||
barf: 'BARF', snack: 'Snack', sonstiges: 'Sonstiges',
|
||||
};
|
||||
|
||||
const KAT_LABELS = {
|
||||
gastro_negativ: 'Magen & Darm',
|
||||
haut_negativ: 'Haut & Fell',
|
||||
allgemein_negativ: 'Allgemein',
|
||||
positiv: 'Positiv',
|
||||
sonstiges: 'Sonstiges',
|
||||
};
|
||||
|
||||
const hinweisHtml = data.hinweis ? `
|
||||
<div style="display:flex;align-items:flex-start;gap:var(--space-2);
|
||||
background:rgba(245,158,11,0.12);border:1px solid var(--c-warning,#f59e0b);
|
||||
border-radius:var(--radius-md);padding:var(--space-3);
|
||||
margin-bottom:var(--space-3);font-size:var(--text-xs);
|
||||
color:var(--c-text);line-height:1.5">
|
||||
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-warning,#f59e0b);margin-top:1px">
|
||||
<use href="/icons/phosphor.svg#warning-circle"></use>
|
||||
</svg>
|
||||
<span>${_esc(data.hinweis)}</span>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
analyseEl.innerHTML = `
|
||||
<h4 style="font-size:var(--text-sm);font-weight:700;margin-bottom:var(--space-3);color:var(--c-text)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#chart-bar"></use></svg>
|
||||
Verträglichkeits-Analyse
|
||||
<span style="font-weight:400;color:var(--c-text-muted);font-size:var(--text-xs)">
|
||||
(${data.eintraege_count} Mahlzeiten, ${data.reaktionen_count} Reaktionen)
|
||||
</span>
|
||||
</h4>
|
||||
${hinweisHtml}
|
||||
<div style="display:grid;gap:var(--space-2)">
|
||||
${data.futter.map(f => {
|
||||
const cfg = STATUS_CFG[f.status] || STATUS_CFG.neu;
|
||||
// Symptom-Kategorien des Futters als Chips
|
||||
const katChips = Object.entries(f.kategorien || {})
|
||||
.filter(([kat]) => kat !== 'positiv')
|
||||
.map(([kat, cnt]) => {
|
||||
const isHaut = kat === 'haut_negativ';
|
||||
const isGastro = kat === 'gastro_negativ';
|
||||
const chipColor = isHaut ? 'var(--c-warning,#f59e0b)' :
|
||||
isGastro ? 'var(--c-danger,#ef4444)' :
|
||||
'var(--c-text-muted)';
|
||||
return `<span style="font-size:10px;font-weight:600;padding:2px 6px;
|
||||
border-radius:999px;border:1px solid ${chipColor};
|
||||
color:${chipColor};white-space:nowrap">
|
||||
${_esc(KAT_LABELS[kat] || kat)} ×${cnt}
|
||||
</span>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div style="background:${cfg.bg};border:1px solid ${cfg.color};
|
||||
border-radius:var(--radius-md);padding:var(--space-3);
|
||||
display:flex;align-items:center;justify-content:space-between;gap:var(--space-2)">
|
||||
<div style="min-width:0;flex:1">
|
||||
<div style="font-weight:600;font-size:var(--text-sm);color:var(--c-text);
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
||||
${_esc(f.name)}
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||
${_esc(TYP_LABELS[f.typ] || f.typ)} · ${f.mahlzeiten} Mahlzeit${f.mahlzeiten !== 1 ? 'en' : ''}
|
||||
${f.status !== 'neu' ? `· <span style="color:var(--c-success,#22c55e)">+${f.positiv}</span> / <span style="color:var(--c-danger,#ef4444)">-${f.negativ}</span>` : ''}
|
||||
</div>
|
||||
${katChips ? `<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">${katChips}</div>` : ''}
|
||||
</div>
|
||||
<span style="flex-shrink:0;font-size:var(--text-xs);font-weight:700;
|
||||
color:${cfg.color};white-space:nowrap">
|
||||
${_esc(cfg.label)}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function _loadVerlauf(el, dog) {
|
||||
const verlaufEl = el.querySelector('#vert-verlauf');
|
||||
if (!verlaufEl) return;
|
||||
|
||||
let eintraege = [], reaktionen = [];
|
||||
try {
|
||||
[eintraege, reaktionen] = await Promise.all([
|
||||
API.dogs.futterList(dog.id),
|
||||
API.dogs.reaktionList(dog.id),
|
||||
]);
|
||||
} catch (_) { return; }
|
||||
|
||||
// Letzten 10 Futter + 5 Reaktionen, gemischt chronologisch
|
||||
const items = [
|
||||
...(eintraege || []).slice(0, 10).map(e => ({ ...e, _art: 'futter' })),
|
||||
...(reaktionen || []).slice(0, 5).map(r => ({ ...r, _art: 'reaktion' })),
|
||||
].sort((a, b) => {
|
||||
const ta = `${a.datum}T${a.uhrzeit}`;
|
||||
const tb = `${b.datum}T${b.uhrzeit}`;
|
||||
return tb.localeCompare(ta);
|
||||
});
|
||||
|
||||
if (items.length === 0) {
|
||||
verlaufEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const REAK_LABELS = {
|
||||
// Positiv
|
||||
verdauung_gut: 'Gute Verdauung',
|
||||
energie_hoch: 'Viel Energie',
|
||||
fell_glaenzend: 'Glänzendes Fell',
|
||||
// Gastro
|
||||
erbrechen: 'Erbrechen',
|
||||
durchfall: 'Durchfall',
|
||||
blaehungen: 'Blähungen',
|
||||
weicher_stuhl: 'Weicher Stuhl',
|
||||
appetitlosigkeit: 'Appetitlosigkeit',
|
||||
// Haut & Fell
|
||||
juckreiz: 'Juckreiz / Kratzen',
|
||||
haarausfall: 'Haarausfall',
|
||||
stumpfes_fell: 'Stumpfes Fell',
|
||||
schuppenbildung: 'Schuppenbildung',
|
||||
roetungen: 'Hautrötungen / Entzündung',
|
||||
pfotenlecken: 'Pfoten lecken (chronisch)',
|
||||
ohrentzuendung: 'Ohrentzündung',
|
||||
fettiges_fell: 'Fettiges Fell / Seborrhö',
|
||||
// Allgemein
|
||||
schlappheit: 'Schlappheit / Apathie',
|
||||
nervositaet: 'Nervosität / Unruhe',
|
||||
viel_trinken: 'Ungewöhnlich viel trinken',
|
||||
sonstiges: 'Sonstiges',
|
||||
};
|
||||
const NEGATIV_TYPEN = new Set([
|
||||
'erbrechen','durchfall','blaehungen','weicher_stuhl','appetitlosigkeit',
|
||||
'juckreiz','haarausfall','stumpfes_fell','schuppenbildung','roetungen',
|
||||
'pfotenlecken','ohrentzuendung','fettiges_fell',
|
||||
'schlappheit','nervositaet','viel_trinken',
|
||||
]);
|
||||
const POSITIV_TYPEN = new Set(['verdauung_gut','energie_hoch','fell_glaenzend']);
|
||||
|
||||
verlaufEl.innerHTML = `
|
||||
<h4 style="font-size:var(--text-sm);font-weight:700;margin-bottom:var(--space-2);color:var(--c-text)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clock-counter-clockwise"></use></svg>
|
||||
Verlauf
|
||||
</h4>
|
||||
<div style="display:grid;gap:var(--space-2)">
|
||||
${items.map(item => {
|
||||
if (item._art === 'futter') {
|
||||
return `
|
||||
<div data-futter-id="${item.id}"
|
||||
style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
|
||||
display:flex;align-items:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-primary)">
|
||||
<use href="/icons/phosphor.svg#bowl-food"></use>
|
||||
</svg>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(item.futter_name)}</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||
${_esc(item.datum)} ${_esc(item.uhrzeit)}
|
||||
${item.menge_g ? ` · ${item.menge_g} g` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-icon vert-del-futter" data-id="${item.id}"
|
||||
style="flex-shrink:0;background:none;border:none;cursor:pointer;padding:4px;
|
||||
color:var(--c-text-muted)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
const isNeg = NEGATIV_TYPEN.has(item.reaktion_typ);
|
||||
const isPos = POSITIV_TYPEN.has(item.reaktion_typ);
|
||||
const col = isNeg ? 'var(--c-danger,#ef4444)' : isPos ? 'var(--c-success,#22c55e)' : 'var(--c-text-muted)';
|
||||
return `
|
||||
<div data-reaktion-id="${item.id}"
|
||||
style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
|
||||
display:flex;align-items:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:${col}">
|
||||
<use href="/icons/phosphor.svg#heartbeat"></use>
|
||||
</svg>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:600;font-size:var(--text-sm);color:${col}">
|
||||
${_esc(REAK_LABELS[item.reaktion_typ] || item.reaktion_typ)}
|
||||
<span style="font-weight:400;color:var(--c-text-muted)">(${item.intensitaet}/5)</span>
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||
${_esc(item.datum)} ${_esc(item.uhrzeit)}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-icon vert-del-reaktion" data-id="${item.id}"
|
||||
style="flex-shrink:0;background:none;border:none;cursor:pointer;padding:4px;
|
||||
color:var(--c-text-muted)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Löschen-Buttons
|
||||
verlaufEl.querySelectorAll('.vert-del-futter').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!window.confirm('Eintrag löschen?')) return;
|
||||
try {
|
||||
await API.dogs.futterDelete(dog.id, parseInt(btn.dataset.id));
|
||||
UI.toast.success('Eintrag gelöscht.');
|
||||
const tabEl = _container.querySelector('#ern-tab-content');
|
||||
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler.');
|
||||
}
|
||||
});
|
||||
});
|
||||
verlaufEl.querySelectorAll('.vert-del-reaktion').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!window.confirm('Reaktion löschen?')) return;
|
||||
try {
|
||||
await API.dogs.reaktionDelete(dog.id, parseInt(btn.dataset.id));
|
||||
UI.toast.success('Reaktion gelöscht.');
|
||||
const tabEl = _container.querySelector('#ern-tab-content');
|
||||
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// PUBLIC API
|
||||
// ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ window.Page_settings = (() => {
|
|||
<span>Hunde-Profile</span>
|
||||
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
||||
</div>
|
||||
<div id="settings-erinnerungen-wrap"></div>
|
||||
<div class="sidebar-item" id="settings-push-btn"
|
||||
style="padding:var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bell"></use></svg>
|
||||
|
|
@ -444,6 +445,38 @@ window.Page_settings = (() => {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Verstorbene Hunde in Erinnerungen-Sektion laden
|
||||
API.get('/dogs/verstorben').then(dogs => {
|
||||
const el = document.getElementById('settings-erinnerungen-wrap');
|
||||
if (!el || !dogs.length) return;
|
||||
el.innerHTML = dogs.map(d => {
|
||||
const av = d.foto_url
|
||||
? `<img src="${_esc(d.foto_url)}" style="width:36px;height:36px;border-radius:50%;object-fit:cover;flex-shrink:0">`
|
||||
: `<div style="width:36px;height:36px;border-radius:50%;background:var(--c-surface-2);display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:18px;height:18px;color:var(--c-text-muted)" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>
|
||||
</div>`;
|
||||
const jahr = d.verstorben_am ? d.verstorben_am.slice(0, 4) : '';
|
||||
return `
|
||||
<div class="sidebar-item settings-erinnerung-btn" data-dog-id="${d.id}" data-dog-name="${_esc(d.name)}"
|
||||
style="padding:var(--space-3) var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border);cursor:pointer">
|
||||
${av}
|
||||
<div style="display:flex;flex-direction:column;gap:1px;flex:1;min-width:0">
|
||||
<span style="font-weight:600;font-size:var(--text-sm)">${_esc(d.name)}</span>
|
||||
<span style="font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||
<svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true"><use href="/icons/phosphor.svg#heart-break"></use></svg>
|
||||
Erinnerungen${jahr ? ' · ' + jahr : ''}
|
||||
</span>
|
||||
</div>
|
||||
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
el.querySelectorAll('.settings-erinnerung-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => _openGedenkseite(
|
||||
parseInt(btn.dataset.dogId), btn.dataset.dogName
|
||||
));
|
||||
});
|
||||
}).catch(() => {});
|
||||
|
||||
// Achievements laden (Streak + Stats + Badges)
|
||||
API.get('/achievements/me').then(a => {
|
||||
const statsEl = document.getElementById('settings-stats-body');
|
||||
|
|
@ -1456,6 +1489,80 @@ window.Page_settings = (() => {
|
|||
} catch { el.innerHTML = ''; }
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// GEDENKSEITE — für verstorbene Hunde
|
||||
// ----------------------------------------------------------
|
||||
async function _openGedenkseite(dogId, dogName) {
|
||||
UI.modal.open({ title: `Erinnerungen an ${_esc(dogName)}`, body: `
|
||||
<div style="text-align:center;padding:var(--space-4)">
|
||||
<svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary);animation:spin 1s linear infinite" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#spinner"></use>
|
||||
</svg>
|
||||
</div>` });
|
||||
|
||||
let data;
|
||||
try { data = await API.get(`/dogs/${dogId}/gedenkseite`); }
|
||||
catch { UI.modal.close(); return; }
|
||||
|
||||
const d = data;
|
||||
const av = d.dog.foto_url
|
||||
? `<img src="${_esc(d.dog.foto_url)}" style="width:100px;height:100px;border-radius:50%;object-fit:cover;border:3px solid var(--c-primary)">`
|
||||
: `<div style="width:100px;height:100px;border-radius:50%;background:var(--c-primary-subtle);display:flex;align-items:center;justify-content:center;border:3px solid var(--c-primary)"><svg class="ph-icon" style="width:48px;height:48px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg></div>`;
|
||||
|
||||
const photoGrid = d.photos?.length ? `
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px;margin:var(--space-4) 0">
|
||||
${d.photos.map(url => `<img src="${_esc(url)}" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:6px">`).join('')}
|
||||
</div>` : '';
|
||||
|
||||
const statsHtml = `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin:var(--space-4) 0">
|
||||
${d.km_total ? `<div class="card" style="padding:var(--space-3);text-align:center">
|
||||
<div style="font-size:var(--text-xl);font-weight:800;color:var(--c-primary)">${d.km_total}</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">km zusammen</div>
|
||||
</div>` : ''}
|
||||
${d.diary_count ? `<div class="card" style="padding:var(--space-3);text-align:center">
|
||||
<div style="font-size:var(--text-xl);font-weight:800;color:var(--c-primary)">${d.diary_count}</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">Tagebucheinträge</div>
|
||||
</div>` : ''}
|
||||
${d.gemeinsam_tage ? `<div class="card" style="padding:var(--space-3);text-align:center">
|
||||
<div style="font-size:var(--text-xl);font-weight:800;color:var(--c-primary)">${d.gemeinsam_tage}</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">gemeinsame Tage</div>
|
||||
</div>` : ''}
|
||||
</div>`;
|
||||
|
||||
const passed = d.dog.verstorben_am;
|
||||
const passedStr = passed
|
||||
? new Date(passed).toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' })
|
||||
: '';
|
||||
|
||||
UI.modal.open({
|
||||
title: `Erinnerungen an ${_esc(d.dog.name)}`,
|
||||
body: `
|
||||
<div style="text-align:center;margin-bottom:var(--space-4)">
|
||||
${av}
|
||||
<div style="margin-top:var(--space-3);font-size:var(--text-lg);font-weight:700">${_esc(d.dog.name)}</div>
|
||||
${passedStr ? `<div style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:4px">
|
||||
<svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#heart-break"></use></svg>
|
||||
${passedStr}
|
||||
</div>` : ''}
|
||||
</div>
|
||||
${statsHtml}
|
||||
${photoGrid}
|
||||
<div style="background:var(--c-primary-subtle);border-left:3px solid var(--c-primary);
|
||||
border-radius:0 var(--radius-md) var(--radius-md) 0;padding:var(--space-4);margin:var(--space-4) 0">
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0;line-height:1.6">
|
||||
Der Schmerz über den Verlust eines Hundes ist real und tief. Lass dich trauern — die Erinnerungen bleiben immer bei dir.
|
||||
</p>
|
||||
</div>
|
||||
${d.ki_abschied ? `<div style="font-style:italic;font-size:var(--text-sm);color:var(--c-text-secondary);
|
||||
line-height:1.7;padding:var(--space-3);background:var(--c-surface);
|
||||
border-radius:var(--radius-md);border:1px solid var(--c-border)">
|
||||
"${_esc(d.ki_abschied)}"
|
||||
</div>` : ''}
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// NICHT EINGELOGGT — Login / Registrierung
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v856';
|
||||
const CACHE_VERSION = 'by-v872';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||
|
|
@ -123,6 +123,7 @@ const _CACHEABLE_GET = [
|
|||
/^\/api\/dogs\/\d+\/health(?!\/ki-)/, // ki-berichte + ki-zusammenfassung nie cachen
|
||||
/^\/api\/training\/exercises/,
|
||||
/^\/api\/training\/progress/,
|
||||
/^\/api\/training\/plan-progress/,
|
||||
/^\/api\/wiki\/rassen/,
|
||||
/^\/api\/dogs\/\d+\/diary\/stats/,
|
||||
// Drei Welten — offline-fähig
|
||||
|
|
@ -136,14 +137,18 @@ function _isCacheableGet(pathname) {
|
|||
}
|
||||
|
||||
// Cache-TTL: stabile Daten länger, dynamische kürzer
|
||||
const _STABLE_GET = [/^\/api\/training\/exercises/, /^\/api\/wiki\/rassen/];
|
||||
const _STABLE_GET = [/^\/api\/training\/exercises/, /^\/api\/wiki\/rassen/];
|
||||
const _SHORT_GET = [/^\/api\/training\/progress/, /^\/api\/training\/plan-progress/];
|
||||
const _TTL_STABLE = 60 * 60 * 1000; // 1 Stunde
|
||||
const _TTL_SHORT = 30 * 1000; // 30 Sekunden (Fortschritte, hund-spezifisch)
|
||||
const _TTL_DEFAULT = 5 * 60 * 1000; // 5 Minuten
|
||||
|
||||
const _cacheTs = new Map(); // pathname → timestamp (in-memory, ok bei SW-Neustart)
|
||||
|
||||
function _cacheTTL(pathname) {
|
||||
return _STABLE_GET.some(re => re.test(pathname)) ? _TTL_STABLE : _TTL_DEFAULT;
|
||||
if (_STABLE_GET.some(re => re.test(pathname))) return _TTL_STABLE;
|
||||
if (_SHORT_GET.some(re => re.test(pathname))) return _TTL_SHORT;
|
||||
return _TTL_DEFAULT;
|
||||
}
|
||||
function _cacheStale(pathname) {
|
||||
const ts = _cacheTs.get(pathname);
|
||||
|
|
@ -359,6 +364,12 @@ self.addEventListener('message', event => {
|
|||
self.skipWaiting();
|
||||
return;
|
||||
}
|
||||
if (event.data?.type === 'INVALIDATE_CACHE') {
|
||||
// Cache-Timestamps für angegebene Pfade zurücksetzen → nächster Request geht ans Netz
|
||||
const paths = event.data.paths || [];
|
||||
paths.forEach(p => _cacheTs.delete(p));
|
||||
return;
|
||||
}
|
||||
if (event.data?.type === 'PROCESS_QUEUE') {
|
||||
event.waitUntil(_processQueue());
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue