diff --git a/backend/main.py b/backend/main.py index bed79a5..a883e9b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -325,6 +325,18 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") os.makedirs(MEDIA_DIR, exist_ok=True) app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") +APP_VER = "715" # muss mit APP_VER in app.js übereinstimmen + +@app.get("/api/version") +async def app_version(): + """Aktuelle Frontend-Version — wird beim App-Start gecheckt.""" + return Response( + content=f'{{"version":"{APP_VER}"}}', + media_type="application/json", + headers={"Cache-Control": "no-store"}, + ) + + @app.get("/stats/script.js") async def umami_script_proxy(): async with httpx.AsyncClient(timeout=10) as client: diff --git a/backend/media_utils.py b/backend/media_utils.py index 8a8698f..4cb2e28 100644 --- a/backend/media_utils.py +++ b/backend/media_utils.py @@ -178,6 +178,17 @@ def generate_preview(data: bytes, ext: str) -> bytes | None: return None +def get_image_size(data: bytes) -> tuple[int, int] | None: + """Gibt (width, height) eines Bildes zurück, oder None bei Fehler.""" + try: + from PIL import Image, ImageOps + img = Image.open(io.BytesIO(data)) + img = ImageOps.exif_transpose(img) + return img.size # (width, height) + except Exception: + return None + + def preview_url_from(url: str | None) -> str | None: """Leitet die Preview-URL aus der Original-URL ab (fügt _preview vor Extension ein). Gibt None für Videos, PDFs, bereits generierte Previews und leere URLs zurück.""" diff --git a/backend/routes/achievements.py b/backend/routes/achievements.py index 0a2988e..e8d0cba 100644 --- a/backend/routes/achievements.py +++ b/backend/routes/achievements.py @@ -203,11 +203,11 @@ def check_and_award(user_id: int, conn): "SELECT current_streak FROM users WHERE id=?", (user_id,) ).fetchone() - # Wetter-Tapferkeit: Diary-Einträge bei schlechtem Wetter + # Wetter-Tapferkeit: Diary-Einträge bei schlechtem Wetter (über Dog-Join) wetter_row = conn.execute(""" SELECT COUNT(*) AS cnt FROM diary d - LEFT JOIN diary_dogs dd ON dd.diary_id = d.id - WHERE d.user_id = ? + JOIN dogs dog ON dog.id = d.dog_id + WHERE dog.user_id = ? AND d.weather_json IS NOT NULL AND ( CAST(json_extract(d.weather_json, '$.precip_prob') AS INTEGER) > 60 @@ -216,23 +216,28 @@ def check_and_award(user_id: int, conn): ) """, (user_id,)).fetchone() - # Jahreszeiten: Anzahl Jahreszeiten mit mind. 5 Diary-Einträgen + # Jahreszeiten: Anzahl Jahreszeiten mit mind. 5 Diary-Einträgen (über Dog-Join) jahreszeiten_row = conn.execute(""" SELECT - (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) AS jahreszeiten_score FROM (SELECT 1) """, (user_id, user_id, user_id, user_id)).fetchone() - # Schnee: Diary-Einträge bei Schnee (weathercode 71-77) + # Schnee: Diary-Einträge bei Schnee (weathercode 71-77, über Dog-Join) schnee_row = conn.execute(""" - SELECT COUNT(*) AS cnt FROM diary - WHERE user_id = ? - AND weather_json IS NOT NULL - AND CAST(json_extract(weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 + SELECT COUNT(*) AS cnt FROM diary d + JOIN dogs dog ON dog.id = d.dog_id + WHERE dog.user_id = ? + AND d.weather_json IS NOT NULL + AND CAST(json_extract(d.weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 """, (user_id,)).fetchone() metrics = { diff --git a/backend/routes/osm.py b/backend/routes/osm.py index de4cb45..998cd4b 100644 --- a/backend/routes/osm.py +++ b/backend/routes/osm.py @@ -280,9 +280,14 @@ class UserPoiIn(BaseModel): ALLOWED_TYPES = { 'waste_basket', 'drinking_water', 'dog_park', 'giftkoeder', # Giftköder (exklusiv, kein Kombi) + 'gefahr', # Allgemeine Gefahr / Hinweis + 'freilauf', # Freilauffläche + 'restaurant', # Hundefreundliches Restaurant / Café + 'shop', # Hundefreundlicher Shop + 'tierarzt', # Tierarzt / Tierklinik + 'hundeschule', # Hundeschule / Trainer 'kotbeutel', # Kotbeutelspender 'bank', # Sitzbank - 'gefahr', # Allgemeine Gefahr / Hinweis 'parkplatz', # Hundefreundlicher Parkplatz 'treffpunkt', # Treffpunkt für Hundehalter 'sonstiges', diff --git a/backend/routes/weather.py b/backend/routes/weather.py index ba45306..2167b19 100644 --- a/backend/routes/weather.py +++ b/backend/routes/weather.py @@ -43,7 +43,8 @@ async def weather_records(user=Depends(get_current_user)): rows = conn.execute(""" SELECT d.datum, d.weather_json, d.titel FROM diary d - WHERE d.user_id = ? AND d.weather_json IS NOT NULL + JOIN dogs dog ON dog.id = d.dog_id + WHERE dog.user_id = ? AND d.weather_json IS NOT NULL ORDER BY d.datum ASC """, (uid,)).fetchall() diff --git a/backend/routes/widget.py b/backend/routes/widget.py index 4af2473..2c04ae8 100644 --- a/backend/routes/widget.py +++ b/backend/routes/widget.py @@ -1,6 +1,6 @@ """BAN YARO — Widget-Snapshot + Tagesspruch Endpoints""" -import json, random +import json from datetime import date from fastapi import APIRouter, Depends, Query from typing import Optional @@ -59,7 +59,8 @@ async def widget_snapshot(user=Depends(get_current_user)): (dog_id,) ).fetchall() - random_photo = dict(random.choice(photos)) if photos else None + day_num = (date.today() - date(2024, 1, 1)).days + random_photo = dict(photos[day_num % len(photos)]) if photos else None # Anzahl überfälliger Erinnerungen overdue = conn.execute( diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 4b46952..d3dcc37 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -8061,28 +8061,35 @@ svg.empty-state-icon { backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-radius: 16px; - padding: 14px 6px 11px; + padding: 12px 6px; text-align: center; cursor: pointer; display: flex; flex-direction: column; align-items: center; - gap: 7px; + justify-content: center; + gap: 6px; color: white; transition: background 0.12s, transform 0.1s; -webkit-tap-highlight-color: transparent; user-select: none; + min-height: 80px; /* alle Chips gleich hoch */ } .world-chip:active { background: rgba(0, 0, 0, 0.6); transform: scale(0.93); } -.world-chip svg { color: white; } +.world-chip svg { color: white; flex-shrink: 0; } .world-chip-label { font-size: 10px; font-weight: 600; color: rgba(255, 255, 255, 0.9); line-height: 1.2; + max-height: 2.4em; /* max. 2 Zeilen */ + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } /* Chip-Umrandung je Welt */ diff --git a/backend/static/img/banyaro/fruehling_playdate.webp b/backend/static/img/banyaro/fruehling_playdate.webp new file mode 100644 index 0000000..7defe9e Binary files /dev/null and b/backend/static/img/banyaro/fruehling_playdate.webp differ diff --git a/backend/static/img/banyaro/herbst_bach.webp b/backend/static/img/banyaro/herbst_bach.webp new file mode 100644 index 0000000..5b7a594 Binary files /dev/null and b/backend/static/img/banyaro/herbst_bach.webp differ diff --git a/backend/static/img/banyaro/herbst_baum.webp b/backend/static/img/banyaro/herbst_baum.webp new file mode 100644 index 0000000..4ea4312 Binary files /dev/null and b/backend/static/img/banyaro/herbst_baum.webp differ diff --git a/backend/static/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg b/backend/static/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg new file mode 100644 index 0000000..eb4e55a Binary files /dev/null and b/backend/static/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg differ diff --git a/backend/static/img/banyaro/hires/banyaro_herbst_bach_hires.jpg b/backend/static/img/banyaro/hires/banyaro_herbst_bach_hires.jpg new file mode 100644 index 0000000..aab4923 Binary files /dev/null and b/backend/static/img/banyaro/hires/banyaro_herbst_bach_hires.jpg differ diff --git a/backend/static/img/banyaro/hires/banyaro_herbst_baum_hires.jpg b/backend/static/img/banyaro/hires/banyaro_herbst_baum_hires.jpg new file mode 100644 index 0000000..d14e80d Binary files /dev/null and b/backend/static/img/banyaro/hires/banyaro_herbst_baum_hires.jpg differ diff --git a/backend/static/img/banyaro/hires/banyaro_winter_schnee_hires.jpg b/backend/static/img/banyaro/hires/banyaro_winter_schnee_hires.jpg new file mode 100644 index 0000000..155d65b Binary files /dev/null and b/backend/static/img/banyaro/hires/banyaro_winter_schnee_hires.jpg differ diff --git a/backend/static/img/banyaro/winter_schnee.webp b/backend/static/img/banyaro/winter_schnee.webp new file mode 100644 index 0000000..87269d4 Binary files /dev/null and b/backend/static/img/banyaro/winter_schnee.webp differ diff --git a/backend/static/index.html b/backend/static/index.html index 77f0433..242e2b7 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -93,9 +93,9 @@ - - - + + +
@@ -574,7 +574,7 @@ - + @@ -676,5 +676,6 @@ } +