diff --git a/backend/auth.py b/backend/auth.py index a4d5af3..55c63fc 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -87,7 +87,7 @@ def get_current_user( user_id = int(payload["sub"]) with db() as conn: row = conn.execute( - "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, gassi_stunde_push, breeder_status, is_founder, is_partner, founder_number, email_verified, luna_trial_until FROM users WHERE id=?", + "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner, founder_number, email_verified, luna_trial_until FROM users WHERE id=?", (user_id,) ).fetchone() diff --git a/backend/database.py b/backend/database.py index 5e38a96..eeb1add 100644 --- a/backend/database.py +++ b/backend/database.py @@ -540,9 +540,6 @@ def _migrate(conn_factory): ("pflege_tipps", "fell_pflege_art", "TEXT"), # Wiki-Foto-Einreichungen: Bildrechte-Bestätigung ("wiki_foto_submissions", "rights_confirmed", "INTEGER NOT NULL DEFAULT 0"), - # Tagebuch-Medien: Bildmaße für Querformat-Filter - ("diary_media", "img_width", "INTEGER"), - ("diary_media", "img_height", "INTEGER"), # Tagebuch: Wetter + POI-Metadaten beim Eintrag ("diary", "weather_json", "TEXT"), ("diary", "poi_json", "TEXT"), @@ -571,11 +568,6 @@ def _migrate(conn_factory): # Passwort-Zurücksetzen ("users", "password_reset_token", "TEXT"), ("users", "password_reset_expires", "TEXT"), - # Fell-Typ für personalisierte Wetter-Hinweise - ("dogs", "fell_typ", "TEXT"), # kurz|mittel|lang|drahtaar|doppel|nackt - # Tierarzt-Bewertungen: Durchschnitt + Anzahl am Tierarzt-Datensatz - ("tieraerzte", "avg_rating", "REAL DEFAULT 0"), - ("tieraerzte", "anz_bewertungen", "INTEGER DEFAULT 0"), ] with conn_factory() as conn: for table, column, col_type in migrations: @@ -1931,44 +1923,6 @@ def _migrate(conn_factory): ) """) - # Welten-Chip-Konfiguration pro User - existing_u = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()] - if 'world_config' not in existing_u: - conn.execute("ALTER TABLE users ADD COLUMN world_config TEXT") - - # Tagessprüche-Pool - conn.executescript(""" - CREATE TABLE IF NOT EXISTS daily_quotes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - text TEXT NOT NULL, - autor TEXT, - kategorie TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - CREATE INDEX IF NOT EXISTS idx_dq_kategorie ON daily_quotes(kategorie); - """) - - # Goldene Gassi-Stunde: User-Einstellung - existing_u = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()] - if 'gassi_stunde_push' not in existing_u: - conn.execute("ALTER TABLE users ADD COLUMN gassi_stunde_push INTEGER NOT NULL DEFAULT 0") - logger.info("Migration: users.gassi_stunde_push bereit.") - - # Futter-Profil - conn.executescript(""" - CREATE TABLE IF NOT EXISTS futter_profil ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - dog_id INTEGER REFERENCES dogs(id) ON DELETE CASCADE UNIQUE, - futter_typ TEXT, - marke TEXT, - kcal_tag INTEGER, - portionen INTEGER DEFAULT 2, - notizen TEXT, - updated_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - """) - logger.info("Migration: futter_profil bereit.") - # Wiederkehrende Ausgaben (Daueraufträge) conn.executescript(""" CREATE TABLE IF NOT EXISTS recurring_expenses ( @@ -1986,75 +1940,3 @@ def _migrate(conn_factory): ); CREATE INDEX IF NOT EXISTS idx_recurring_user ON recurring_expenses(user_id, aktiv); """) - - # ---- Tierarzt-Bewertungen ---- - conn.executescript(""" - CREATE TABLE IF NOT EXISTS tierarzt_bewertungen ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tierarzt_id INTEGER NOT NULL REFERENCES tieraerzte(id) ON DELETE CASCADE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - gesamt INTEGER NOT NULL CHECK(gesamt BETWEEN 1 AND 5), - wartezeit INTEGER CHECK(wartezeit BETWEEN 1 AND 5), - freundlichkeit INTEGER CHECK(freundlichkeit BETWEEN 1 AND 5), - kompetenz INTEGER CHECK(kompetenz BETWEEN 1 AND 5), - text TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE(tierarzt_id, user_id) - ); - CREATE INDEX IF NOT EXISTS idx_tierarzt_bew_arzt - ON tierarzt_bewertungen(tierarzt_id); - """) - - # ---- Feature: Foto-Challenge der Woche ---- - conn.executescript(""" - CREATE TABLE IF NOT EXISTS foto_challenge ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - thema TEXT NOT NULL, - beschreibung TEXT, - start_date TEXT NOT NULL, - end_date TEXT NOT NULL, - created_by INTEGER REFERENCES users(id), - created_at TEXT DEFAULT (datetime('now')) - ); - CREATE TABLE IF NOT EXISTS challenge_submissions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - challenge_id INTEGER REFERENCES foto_challenge(id) ON DELETE CASCADE, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, - foto_url TEXT NOT NULL, - caption TEXT, - votes INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')), - UNIQUE(challenge_id, user_id) - ); - CREATE TABLE IF NOT EXISTS challenge_votes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - submission_id INTEGER REFERENCES challenge_submissions(id) ON DELETE CASCADE, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - UNIQUE(submission_id, user_id) - ); - CREATE INDEX IF NOT EXISTS idx_challenge_sub_chal - ON challenge_submissions(challenge_id, created_at DESC); - """) - logger.info("Migration: Foto-Challenge-Tabellen bereit.") - - # ---- Feature: Gassi-Zeiten-Pool ---- - conn.executescript(""" - CREATE TABLE IF NOT EXISTS gassi_zeiten ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, - wochentage TEXT NOT NULL, - uhrzeit TEXT NOT NULL, - ort_name TEXT, - lat REAL, - lon REAL, - radius_m INTEGER DEFAULT 500, - notiz TEXT, - aktiv INTEGER DEFAULT 1, - created_at TEXT DEFAULT (datetime('now')) - ); - CREATE INDEX IF NOT EXISTS idx_gassi_zeiten_user - ON gassi_zeiten(user_id, aktiv); - """) - logger.info("Migration: Gassi-Zeiten-Tabelle bereit.") diff --git a/backend/main.py b/backend/main.py index 1d23aef..229a856 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,10 +6,9 @@ import os import html import logging from collections import deque -import httpx from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse, JSONResponse, Response +from fastapi.responses import FileResponse, JSONResponse from starlette.middleware.base import BaseHTTPMiddleware from fastapi.middleware.gzip import GZipMiddleware from brotli_asgi import BrotliMiddleware @@ -44,43 +43,10 @@ logger = logging.getLogger(__name__) # ------------------------------------------------------------------ # Startup / Shutdown # ------------------------------------------------------------------ -def _backfill_image_sizes(): - """Füllt img_width/img_height für alle diary_media-Bilder ohne Maße nach.""" - import io - from database import db - from media_utils import get_image_size - MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") - with db() as conn: - rows = conn.execute( - "SELECT id, url FROM diary_media WHERE media_type='image' AND img_width IS NULL" - ).fetchall() - if not rows: - return - logger.info("Backfill Bildmaße: %d Einträge...", len(rows)) - updated = 0 - for row in rows: - # url ist z.B. /media/diary/xxx.jpg → Pfad: MEDIA_DIR/diary/xxx.jpg - rel = row["url"].removeprefix("/media/") - path = os.path.join(MEDIA_DIR, rel) - try: - with open(path, "rb") as f: - data = f.read() - size = get_image_size(data) - if size: - with db() as conn: - conn.execute( - "UPDATE diary_media SET img_width=?, img_height=? WHERE id=?", - (size[0], size[1], row["id"]) - ) - updated += 1 - except Exception: - pass - logger.info("Backfill Bildmaße abgeschlossen: %d/%d aktualisiert.", updated, len(rows)) @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Ban Yaro startet...") init_db() - _backfill_image_sizes() from routes.movies import seed_movies seed_movies() logger.info(f"KI-Modus: {ki.KI_MODE}") @@ -110,7 +76,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" response.headers["Content-Security-Policy"] = ( "default-src 'self'; " - "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://umami.motocamp.de; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " "style-src 'self' 'unsafe-inline'; " "img-src 'self' data: blob: https:; " "connect-src 'self' https:; " @@ -232,7 +198,6 @@ from routes.adoption import router as adoption_router from routes.health_docs import router as health_docs_router from routes.passport import router as passport_router from routes.playdate import router as playdate_router -from routes.ernaehrung import router as ernaehrung_router app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) @@ -291,7 +256,6 @@ app.include_router(adoption_router, prefix="/api/adoption", ta app.include_router(health_docs_router, prefix="/api/health-docs", tags=["Gesundheitsdokumente"]) app.include_router(passport_router, prefix="/api/passport", tags=["Hundepass"]) app.include_router(playdate_router, prefix="/api/playdate", tags=["Playdate"]) -app.include_router(ernaehrung_router, prefix="/api/dogs", tags=["Ernährung"]) # ------------------------------------------------------------------ @@ -321,27 +285,6 @@ 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.get("/stats/script.js") -async def umami_script_proxy(): - async with httpx.AsyncClient(timeout=10) as client: - r = await client.get("https://umami.motocamp.de/script.js") - return Response(content=r.content, media_type="application/javascript", - headers={"Cache-Control": "public, max-age=86400"}) - -@app.post("/stats/api/send") -async def umami_send_proxy(request: Request): - body = await request.body() - async with httpx.AsyncClient(timeout=10) as client: - r = await client.post( - "https://umami.motocamp.de/api/send", - content=body, - headers={"Content-Type": "application/json", - "User-Agent": request.headers.get("user-agent", "")}, - ) - return Response(content=r.content, status_code=r.status_code, - media_type="application/json") - - @app.get("/robots.txt") async def robots(): return FileResponse(f"{STATIC_DIR}/robots.txt", media_type="text/plain") diff --git a/backend/routes/achievements.py b/backend/routes/achievements.py index 0a2988e..1c0c1e8 100644 --- a/backend/routes/achievements.py +++ b/backend/routes/achievements.py @@ -92,59 +92,6 @@ CATEGORIES = [ ("gold", 10, "Wiki-Fotograf"), ], }, - { - "id": "wetter_tapfer", - "name": "Wetter-Tapferkeit", - "emoji": "⛈️", - "metrik": "wetter_tapfer_score", - "einheit": " Eintrag/Einträge", - "stufen": [ - ("bronze", 1, "Regentrotzdem"), - ("silber", 5, "Wettertrotzer"), - ("gold", 15, "Allwetter-Held"), - ("platin", 30, "Hunde-Wetterheld"), - ], - }, - { - "id": "jahreszeiten", - "name": "Jahreszeiten-Erkunder", - "emoji": "🍃", - "metrik": "jahreszeiten_score", - "einheit": " Jahreszeit(en)", - "stufen": [ - ("bronze", 1, "Frühlings-Erkunder"), - ("silber", 2, "Sommer-Genießer"), - ("gold", 3, "Herbst-Schnüffler"), - ("platin", 4, "Alle-Jahreszeiten"), - ], - }, - { - "id": "schnee_held", - "name": "Schneeheld", - "emoji": "❄️", - "metrik": "schnee_eintraege", - "einheit": " Eintrag/Einträge", - "stufen": [ - ("bronze", 1, "Erster Schnee"), - ("silber", 5, "Schneehund"), - ("gold", 15, "Schneeheld"), - ("platin", 30, "Schneewolf"), - ], - }, - { - "id": "km_lebenswerk", - "name": "Kilometer-Lebenswerk", - "emoji": "🐾", - "metrik": "gesamt_km_lebenswerk", - "einheit": " km", - "icon": "path", - "stufen": [ - ("bronze", 100, "100-km-Club"), - ("silber", 500, "500-km-Wanderer"), - ("gold", 1000, "Tausend-km-Held"), - ("platin", 5000, "Ultraläufer"), - ], - }, ] # Flat-Liste aller Badge-IDs für DB-Kompatibilität @@ -203,48 +150,12 @@ 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_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 = ? - AND d.weather_json IS NOT NULL - AND ( - CAST(json_extract(d.weather_json, '$.precip_prob') AS INTEGER) > 60 - OR CAST(json_extract(d.weather_json, '$.temp_c') AS REAL) < 2 - OR CAST(json_extract(d.weather_json, '$.wind_kmh') AS REAL) > 50 - ) - """, (user_id,)).fetchone() - - # Jahreszeiten: Anzahl Jahreszeiten mit mind. 5 Diary-Einträgen - 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) - 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_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 - """, (user_id,)).fetchone() - metrics = { - "total_km": stats["total_km"] if stats else 0, - "routen": stats["routen"] if stats else 0, - "pois": stats["pois"] if stats else 0, - "streak": (streak_row["current_streak"] if streak_row else 0), - "wiki_fotos": stats["wiki_fotos"] if stats else 0, - "wetter_tapfer_score": wetter_row["cnt"] if wetter_row else 0, - "jahreszeiten_score": (jahreszeiten_row["jahreszeiten_score"] if jahreszeiten_row else 0), - "schnee_eintraege": schnee_row["cnt"] if schnee_row else 0, - "gesamt_km_lebenswerk": stats["total_km"] if stats else 0, + "total_km": stats["total_km"] if stats else 0, + "routen": stats["routen"] if stats else 0, + "pois": stats["pois"] if stats else 0, + "streak": (streak_row["current_streak"] if streak_row else 0), + "wiki_fotos": stats["wiki_fotos"] if stats else 0, } earned = {r["badge_id"] for r in @@ -300,38 +211,6 @@ async def my_achievements(user=Depends(get_current_user)): "SELECT current_streak, max_streak FROM users WHERE id=?", (uid,) ).fetchone() - # Wetter-Tapferkeit - 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 = ? - AND d.weather_json IS NOT NULL - AND ( - CAST(json_extract(d.weather_json, '$.precip_prob') AS INTEGER) > 60 - OR CAST(json_extract(d.weather_json, '$.temp_c') AS REAL) < 2 - OR CAST(json_extract(d.weather_json, '$.wind_kmh') AS REAL) > 50 - ) - """, (uid,)).fetchone() - - # Jahreszeiten - 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) - AS jahreszeiten_score - FROM (SELECT 1) - """, (uid, uid, uid, uid)).fetchone() - - # Schnee-Einträge - 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 - """, (uid,)).fetchone() - earned_rows = conn.execute( "SELECT badge_id FROM user_badges WHERE user_id=?", (uid,) ).fetchall() @@ -351,15 +230,11 @@ async def my_achievements(user=Depends(get_current_user)): """, (stats["punkte"] if stats else 0,)).fetchone() metrics = { - "total_km": stats["total_km"] if stats else 0, - "routen": stats["routen"] if stats else 0, - "pois": stats["pois"] if stats else 0, - "streak": (streak_row["current_streak"] if streak_row else 0), - "wiki_fotos": stats["wiki_fotos"] if stats else 0, - "wetter_tapfer_score": wetter_row["cnt"] if wetter_row else 0, - "jahreszeiten_score": (jahreszeiten_row["jahreszeiten_score"] if jahreszeiten_row else 0), - "schnee_eintraege": schnee_row["cnt"] if schnee_row else 0, - "gesamt_km_lebenswerk": stats["total_km"] if stats else 0, + "total_km": stats["total_km"] if stats else 0, + "routen": stats["routen"] if stats else 0, + "pois": stats["pois"] if stats else 0, + "streak": (streak_row["current_streak"] if streak_row else 0), + "wiki_fotos": stats["wiki_fotos"] if stats else 0, } # Kategorien mit aktuellem Tier + Fortschritt aufbauen diff --git a/backend/routes/diary.py b/backend/routes/diary.py index 6f6cd12..a3dee2b 100644 --- a/backend/routes/diary.py +++ b/backend/routes/diary.py @@ -9,7 +9,7 @@ from auth import get_current_user, require_admin import ki as KI import httpx import weather as weather_mod -from media_utils import convert_media, extract_video_thumb, safe_media_path, validate_upload, extract_gps_from_exif, generate_preview, preview_url_from, get_image_size +from media_utils import convert_media, extract_video_thumb, safe_media_path, validate_upload, extract_gps_from_exif, generate_preview, preview_url_from from timeutils import safe_client_time logger = logging.getLogger(__name__) @@ -30,7 +30,6 @@ class DiaryCreate(BaseModel): location_name: Optional[str] = None is_milestone: bool = False dog_ids: Optional[list[int]] = None # alle Hunde inkl. primär; None = nur primary - weather_json: Optional[str] = None # Client-seitig vorab geholtes Wetter (Fallback wenn kein GPS) class DiaryUpdate(BaseModel): @@ -351,19 +350,6 @@ async def create_diary(dog_id: int, data: DiaryCreate, ) entry = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone() - elif data.weather_json: - # Client hat Wetter vorab geholt (kein GPS-Standort gesetzt) → direkt speichern - try: - json.loads(data.weather_json) # Validierung - with db() as conn: - conn.execute( - "UPDATE diary SET weather_json=? WHERE id=?", - (data.weather_json, entry_id) - ) - entry = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone() - except Exception as exc: - logger.warning("Client-weather_json ungültig: %s", exc) - return _entry_dict(entry, dogs_map, media_map) @@ -706,12 +692,10 @@ async def upload_media(dog_id: int, entry_id: int, media_url = f"/media/diary/{filename}" - # Bildmaße + EXIF-GPS (nur bei Bilddateien) - exif_gps = None - img_size = None + # EXIF-GPS aus Bild extrahieren (nur bei Bilddateien) + exif_gps = None if media_type == "image": exif_gps = extract_gps_from_exif(raw_data) - img_size = get_image_size(raw_data) with db() as conn: # sort_order = nächste freie Position @@ -722,9 +706,8 @@ async def upload_media(dog_id: int, entry_id: int, # Erstes Item eines Eintrags wird automatisch Cover is_cover = 1 if max_order == -1 else 0 conn.execute( - "INSERT INTO diary_media (diary_id, url, media_type, sort_order, is_cover, img_width, img_height) VALUES (?,?,?,?,?,?,?)", - (entry_id, media_url, media_type, max_order + 1, is_cover, - img_size[0] if img_size else None, img_size[1] if img_size else None) + "INSERT INTO diary_media (diary_id, url, media_type, sort_order, is_cover) VALUES (?,?,?,?,?)", + (entry_id, media_url, media_type, max_order + 1, is_cover) ) new_id = conn.execute( "SELECT id FROM diary_media WHERE diary_id=? ORDER BY id DESC LIMIT 1", diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 42b9b32..a44faa0 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -181,29 +181,18 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)): raise HTTPException(404, "Hund nicht gefunden.") # Zufälliges Foto aus den letzten 100 Tagebuchbildern - # Alle Querformat-Fotos (breiter als hoch) des Hundes, stabile Reihenfolge 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, d.id DESC, dm.id ASC""", + ORDER BY d.datum DESC LIMIT 100""", (dog_id,) ).fetchall() - # Fallback: alle Fotos ohne Maß-Filter (Bilder vor dem Backfill) - 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, d.id DESC, dm.id ASC""", - (dog_id,) - ).fetchall() random_photo = None if photos: import datetime as _dt2 - tick = (_dt2.date.today() - _dt2.date(2024, 1, 1)).days - chosen_url = photos[tick % len(photos)]["url"] + day_num = (_dt2.date.today() - _dt2.date(2024, 1, 1)).days + chosen_url = photos[day_num % len(photos)]["url"] random_photo = { "url": chosen_url, "preview_url": preview_url_from(chosen_url), @@ -305,463 +294,6 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)): } -@router.get("/{dog_id}/wrapped") -async def get_dog_wrapped(dog_id: int, year: int = None, user=Depends(get_current_user)): - """Jahresrückblick ('Wrapped') für einen Hund.""" - import json as _json - from datetime import date as _date - - if year is None: - year = _date.today().year - - with db() as conn: - dog = conn.execute( - "SELECT id, name, user_id FROM dogs WHERE id=? AND user_id=?", - (dog_id, user["id"]) - ).fetchone() - if not dog: - raise HTTPException(404, "Hund nicht gefunden.") - - # km gelaufen (eigene Routen des Users) - gesamt_km_row = conn.execute( - "SELECT ROUND(COALESCE(SUM(distanz_km),0),1) AS km FROM routes " - "WHERE user_id=? AND strftime('%Y', created_at)=?", - (user["id"], str(year)) - ).fetchone() - gesamt_km = gesamt_km_row["km"] or 0.0 - - # Gassi-Tage (Distinct Datum in Diary) - gassi_tage = conn.execute( - "SELECT COUNT(DISTINCT datum) AS n FROM diary " - "WHERE dog_id=? AND strftime('%Y', datum)=?", - (dog_id, str(year)) - ).fetchone()["n"] - - # Gesamte Einträge - eintraege_gesamt = conn.execute( - "SELECT COUNT(*) AS n FROM diary " - "WHERE dog_id=? AND strftime('%Y', datum)=?", - (dog_id, str(year)) - ).fetchone()["n"] - - # Fotos gesamt - fotos_gesamt = conn.execute( - "SELECT COUNT(*) AS n FROM diary_media dm " - "JOIN diary d ON d.id=dm.diary_id " - "WHERE d.dog_id=? AND strftime('%Y', d.datum)=? AND dm.media_type='image'", - (dog_id, str(year)) - ).fetchone()["n"] - - # Beste Route (längste distanz) - beste_route_row = conn.execute( - "SELECT MAX(distanz_km) AS km FROM routes " - "WHERE user_id=? AND strftime('%Y', created_at)=?", - (user["id"], str(year)) - ).fetchone() - beste_route = beste_route_row["km"] or 0.0 - - # Lieblingsmonat (meiste diary-Einträge) - monat_rows = conn.execute( - "SELECT strftime('%m', datum) AS monat, COUNT(*) AS n FROM diary " - "WHERE dog_id=? AND strftime('%Y', datum)=? " - "GROUP BY monat ORDER BY n DESC LIMIT 1", - (dog_id, str(year)) - ).fetchone() - lieblings_monat = None - if monat_rows: - _MONATE = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'] - try: - lieblings_monat = _MONATE[int(monat_rows["monat"]) - 1] - except Exception: - pass - - # Lieblingsaktivität (häufigster typ) - typ_row = conn.execute( - "SELECT typ, COUNT(*) AS n FROM diary " - "WHERE dog_id=? AND strftime('%Y', datum)=? " - "GROUP BY typ ORDER BY n DESC LIMIT 1", - (dog_id, str(year)) - ).fetchone() - lieblings_aktivitaet = typ_row["typ"] if typ_row else None - - # Training-Sessions - training_sessions = conn.execute( - "SELECT COUNT(*) AS n FROM training_sessions " - "WHERE dog_id=? AND strftime('%Y', created_at)=?", - (dog_id, str(year)) - ).fetchone()["n"] - - # Gesundheits-Einträge - gesundheit_eintraege = conn.execute( - "SELECT COUNT(*) AS n FROM health " - "WHERE dog_id=? AND strftime('%Y', datum)=?", - (dog_id, str(year)) - ).fetchone()["n"] - - # Wetter-Tapferkeit: Tagebuch-Einträge mit weather_json - wetter_kalt = 0 - wetter_warm = 0 - wetter_rows = conn.execute( - "SELECT weather_json FROM diary " - "WHERE dog_id=? AND strftime('%Y', datum)=? AND weather_json IS NOT NULL", - (dog_id, str(year)) - ).fetchall() - for wr in wetter_rows: - try: - wj = _json.loads(wr["weather_json"]) - temp = wj.get("temp_c") or wj.get("temperature") or wj.get("temp") - if temp is not None: - if float(temp) < 5: - wetter_kalt += 1 - elif float(temp) > 25: - wetter_warm += 1 - except Exception: - pass - - return { - "dog_id": dog_id, - "dog_name": dog["name"], - "year": year, - "gesamt_km": gesamt_km, - "gassi_tage": gassi_tage, - "eintraege_gesamt": eintraege_gesamt, - "fotos_gesamt": fotos_gesamt, - "beste_route": beste_route, - "lieblings_monat": lieblings_monat, - "lieblings_aktivitaet": lieblings_aktivitaet, - "training_sessions": training_sessions, - "gesundheit_eintraege": gesundheit_eintraege, - "wetter_kalt": wetter_kalt, - "wetter_warm": wetter_warm, - } - - -@router.get("/{dog_id}/buch") -async def get_hunde_buch( - dog_id: int, - jahr: int = None, - limit: int = 50, - nur_fotos: bool = False, - nur_meilensteine: bool = False, - user=Depends(get_current_user), -): - """Hunde-Buch: druckbare HTML-Ansicht der schoensten Tagebucheintraege.""" - import json as _json - from datetime import date as _date - from fastapi.responses import HTMLResponse - from html import escape as _esc - - with db() as conn: - dog = conn.execute( - "SELECT id, name, rasse, geburtstag, foto_url FROM dogs WHERE id=? AND user_id=?", - (dog_id, user["id"]) - ).fetchone() - if not dog: - raise HTTPException(404, "Hund nicht gefunden.") - - dog = dict(dog) - - # --- Eintraege laden --- - conditions = ["(d.dog_id=? OR dd.dog_id=?)"] - params: list = [dog_id, dog_id] - - if jahr: - conditions.append("strftime('%Y', d.datum) = ?") - params.append(str(jahr)) - - if nur_meilensteine: - conditions.append("d.is_milestone = 1") - - where = " AND ".join(conditions) - - rows = conn.execute( - f"""SELECT DISTINCT d.id, d.datum, d.titel, d.text, d.tags, - d.gps_lat, d.gps_lon, d.location_name, d.weather_json, - d.is_milestone, - (SELECT dm.url FROM diary_media dm - WHERE dm.diary_id=d.id AND dm.media_type='image' - ORDER BY dm.is_cover DESC, dm.sort_order LIMIT 1) AS cover_url - FROM diary d - LEFT JOIN diary_dogs dd ON dd.diary_id = d.id - WHERE {where} - AND d.datum IS NOT NULL - ORDER BY d.datum ASC""", - params - ).fetchall() - - rows = [dict(r) for r in rows] - - # Filtern: Eintraege mit Foto bevorzugen / nur Fotos-Modus - if nur_fotos: - rows = [r for r in rows if r.get("cover_url")] - else: - # Prioritaet: Meilensteine + Foto-Eintraege; Rest auffuellen bis limit - with_photo = [r for r in rows if r.get("cover_url")] - milestones = [r for r in rows if r.get("is_milestone") and not r.get("cover_url")] - rest = [r for r in rows if not r.get("cover_url") and not r.get("is_milestone")] - rows = with_photo + milestones + rest - rows.sort(key=lambda r: r["datum"] or "") - - rows = rows[:limit] - - # --- Hund-Alter berechnen --- - alter_str = "" - if dog.get("geburtstag"): - try: - geb = _date.fromisoformat(dog["geburtstag"]) - heute = _date.today() - jahre = (heute - geb).days // 365 - alter_str = f"{jahre} Jahre" - except Exception: - pass - - # --- HTML bauen --- - dog_name = _esc(dog["name"] or "Mein Hund") - rasse_str = _esc(dog.get("rasse") or "") - jahr_str = str(jahr) if jahr else "Alle Jahre" - foto_url = dog.get("foto_url") or "" - - cover_img = ( - f'{dog_name}' - if foto_url else - f'
🐾
' - ) - - subtitle_parts = [p for p in [rasse_str, alter_str] if p] - subtitle = " · ".join(subtitle_parts) - - _MONATE = ["Januar","Februar","März","April","Mai","Juni", - "Juli","August","September","Oktober","November","Dezember"] - - def _fmt_datum(iso: str) -> str: - try: - d = _date.fromisoformat(iso) - return f"{d.day}. {_MONATE[d.month - 1]} {d.year}" - except Exception: - return iso or "" - - def _wetter_chip(wj_str: str) -> str: - if not wj_str: - return "" - try: - wj = _json.loads(wj_str) - temp = wj.get("temp_c") or wj.get("temperature") or wj.get("temp") - if temp is None: - return "" - temp_i = int(float(temp)) - emoji = "☀️" if temp_i > 20 else ("🌧️" if temp_i < 10 else "⛅") - return f'{emoji} {temp_i}°C' - except Exception: - return "" - - entries_html = "" - for e in rows: - milestone_class = "milestone" if e.get("is_milestone") else "" - datum_fmt = _fmt_datum(e.get("datum") or "") - titel = _esc(e.get("titel") or "") - text_raw = e.get("text") or "" - text = _esc(text_raw).replace("\n", "
") - wetter = _wetter_chip(e.get("weather_json") or "") - loc = _esc(e.get("location_name") or "") - cover = e.get("cover_url") or "" - - foto_html = "" - if cover: - foto_html = ( - f'
' - f'' - f'
' - ) - - loc_html = f'📍 {loc}' if loc else "" - chips_html = f'
{wetter}{loc_html}
' if (wetter or loc_html) else "" - titel_html = f'
{titel}
' if titel else "" - text_html = f'
{text}
' if text_raw else "" - - entries_html += f""" -
- {foto_html} -
{datum_fmt}
- {titel_html} - {text_html} - {chips_html} -
-""" - - anzahl = len(rows) - html_page = f""" - - - - - Hunde-Buch — {dog_name} - - - - - - -
- {cover_img} -

{dog_name}

- {'
' + subtitle + '
' if subtitle else ''} -
{jahr_str}
-
{anzahl} Einträge
-
- -{entries_html} - - -""" - - return HTMLResponse(content=html_page) - - @router.get("/{dog_id}") async def get_dog(dog_id: int, user=Depends(get_current_user)): with db() as conn: @@ -1090,159 +622,3 @@ async def get_pflege_tipps(dog_id: int, user=Depends(get_current_user)): "kategorien": list(dict.fromkeys(t["kategorie"] for t in result)), "fell_pflege_art": fell_pflege_art_filter, # 'schneiden' | 'trimmen' | None } - - -# ------------------------------------------------------------------ -# LEBENS-TIMELINE -# ------------------------------------------------------------------ -@router.get("/{dog_id}/timeline") -async def get_dog_timeline(dog_id: int, user=Depends(get_current_user)): - """Aggregierte Lebens-Timeline eines Hundes aus allen Datenquellen.""" - import json as _json - - with db() as conn: - dog = conn.execute( - "SELECT id, name, user_id, geburtstag FROM dogs WHERE id=? AND user_id=?", - (dog_id, user["id"]) - ).fetchone() - if not dog: - raise HTTPException(404, "Hund nicht gefunden.") - - events = [] - - with db() as conn: - # --- Tagebuch --- - diary_rows = conn.execute( - """SELECT d.id, d.datum, d.titel, d.typ, d.is_milestone, - dm.url AS foto_url - FROM diary d - LEFT JOIN diary_media dm ON dm.diary_id = d.id AND dm.sort_order = 0 - WHERE d.dog_id=? - ORDER BY d.datum ASC, d.id ASC""", - (dog_id,) - ).fetchall() - - for i, r in enumerate(diary_rows): - events.append({ - "datum": r["datum"], - "kategorie": "tagebuch", - "titel": r["titel"] or ("Tagebucheintrag" if r["typ"] == "eintrag" else str(r["typ"]).capitalize()), - "typ": r["typ"], - "is_first": i == 0, - "is_milestone": bool(r["is_milestone"]), - "foto_url": r["foto_url"], - "ref_id": r["id"], - }) - - # --- Gesundheit --- - health_rows = conn.execute( - """SELECT id, datum, bezeichnung, typ - FROM health - WHERE dog_id=? - ORDER BY datum ASC, id ASC""", - (dog_id,) - ).fetchall() - - typ_seen = {} - for r in health_rows: - t = r["typ"] - is_first = t not in typ_seen - if is_first: - typ_seen[t] = True - events.append({ - "datum": r["datum"], - "kategorie": "gesundheit", - "titel": r["bezeichnung"], - "typ": t, - "is_first": is_first, - "is_milestone": False, - "foto_url": None, - "ref_id": r["id"], - }) - - # --- Training-Sessions --- - ts_rows = conn.execute( - """SELECT id, datum, exercise_name, erfolgsquote, ist_top - FROM training_sessions - WHERE dog_id=? AND user_id=? - ORDER BY datum ASC, id ASC""", - (dog_id, user["id"]) - ).fetchall() - - ts_first = True - ts_best = None - ts_best_score = -1 - for r in ts_rows: - if r["erfolgsquote"] is not None and r["erfolgsquote"] > ts_best_score: - ts_best_score = r["erfolgsquote"] - ts_best = r - - for i, r in enumerate(ts_rows): - is_first = (i == 0) - is_best = ts_best and r["id"] == ts_best["id"] and i > 0 - events.append({ - "datum": r["datum"], - "kategorie": "training", - "titel": r["exercise_name"], - "typ": "training", - "is_first": is_first, - "is_milestone": bool(r["ist_top"]) or is_best, - "foto_url": None, - "ref_id": r["id"], - }) - - # --- Routen --- - 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"],) - ).fetchall() - - route_first = True - route_longest = None - route_max_km = -1 - for r in route_rows: - km = r["distanz_km"] or 0 - if km > route_max_km: - route_max_km = km - route_longest = r - - for i, r in enumerate(route_rows): - is_first = (i == 0) - is_longest = route_longest and r["id"] == route_longest["id"] and i > 0 - events.append({ - "datum": r["datum"], - "kategorie": "route", - "titel": r["name"], - "typ": "route", - "is_first": is_first, - "is_milestone": is_longest, - "foto_url": None, - "ref_id": r["id"], - "distanz_km": r["distanz_km"], - }) - - # Geburtstag des Hundes als erster Eintrag - if dog["geburtstag"]: - events.append({ - "datum": dog["geburtstag"], - "kategorie": "meilenstein", - "titel": f"{dog['name']} wird geboren", - "typ": "geburtstag", - "is_first": True, - "is_milestone": True, - "foto_url": None, - "ref_id": None, - }) - - # Chronologisch sortieren - events.sort(key=lambda e: (e["datum"] or "0000-00-00", e["kategorie"])) - - return { - "dog_name": dog["name"], - "geburtstag": dog["geburtstag"], - "events": events, - } diff --git a/backend/routes/ernaehrung.py b/backend/routes/ernaehrung.py deleted file mode 100644 index c1f850e..0000000 --- a/backend/routes/ernaehrung.py +++ /dev/null @@ -1,145 +0,0 @@ -"""BAN YARO — Ernährungs-Routes""" - -import logging -from fastapi import APIRouter, Depends, HTTPException, Request -from pydantic import BaseModel -from typing import Optional -from database import db -from auth import get_current_user -import ki as ki_module - -router = APIRouter() -logger = logging.getLogger(__name__) - - -# ------------------------------------------------------------------ -# Schemas -# ------------------------------------------------------------------ -class FutterProfilUpdate(BaseModel): - futter_typ: Optional[str] = None # trocken|nass|barf|mix - marke: Optional[str] = None - kcal_tag: Optional[int] = None - portionen: Optional[int] = None - notizen: Optional[str] = None - - -class KiBeratungRequest(BaseModel): - frage: str - dog_name: Optional[str] = None - rasse: Optional[str] = None - alter: Optional[str] = None - gewicht: Optional[float] = None - aktiv: Optional[bool] = None - - -# ------------------------------------------------------------------ -# Hilfsfunktion: Zugriffsprüfung -# ------------------------------------------------------------------ -def _check_dog_access(conn, dog_id: int, user_id: int): - row = conn.execute( - "SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user_id) - ).fetchone() - if not row: - raise HTTPException(404, "Hund nicht gefunden.") - - -# ------------------------------------------------------------------ -# GET /dogs/{dog_id}/ernaehrung -# ------------------------------------------------------------------ -@router.get("/{dog_id}/ernaehrung") -async def get_ernaehrung(dog_id: int, user=Depends(get_current_user)): - with db() as conn: - _check_dog_access(conn, dog_id, user["id"]) - row = conn.execute( - "SELECT * FROM futter_profil WHERE dog_id=?", (dog_id,) - ).fetchone() - if not row: - return {} - return dict(row) - - -# ------------------------------------------------------------------ -# PUT /dogs/{dog_id}/ernaehrung -# ------------------------------------------------------------------ -@router.put("/{dog_id}/ernaehrung") -async def put_ernaehrung(dog_id: int, body: FutterProfilUpdate, - user=Depends(get_current_user)): - with db() as conn: - _check_dog_access(conn, dog_id, user["id"]) - existing = conn.execute( - "SELECT id FROM futter_profil WHERE dog_id=?", (dog_id,) - ).fetchone() - if existing: - conn.execute(""" - UPDATE futter_profil - SET futter_typ=COALESCE(?, futter_typ), - marke=COALESCE(?, marke), - kcal_tag=COALESCE(?, kcal_tag), - portionen=COALESCE(?, portionen), - notizen=COALESCE(?, notizen), - updated_at=datetime('now') - WHERE dog_id=? - """, (body.futter_typ, body.marke, body.kcal_tag, - body.portionen, body.notizen, dog_id)) - else: - conn.execute(""" - INSERT INTO futter_profil - (dog_id, futter_typ, marke, kcal_tag, portionen, notizen) - VALUES (?, ?, ?, ?, ?, ?) - """, (dog_id, body.futter_typ, body.marke, body.kcal_tag, - body.portionen or 2, body.notizen)) - row = conn.execute( - "SELECT * FROM futter_profil WHERE dog_id=?", (dog_id,) - ).fetchone() - return dict(row) - - -# ------------------------------------------------------------------ -# POST /dogs/{dog_id}/ernaehrung/ki-beratung -# ------------------------------------------------------------------ -@router.post("/{dog_id}/ernaehrung/ki-beratung") -async def ki_ernaehrung(dog_id: int, body: KiBeratungRequest, - request: Request, - user=Depends(get_current_user)): - if not body.frage or len(body.frage.strip()) < 3: - raise HTTPException(400, "Bitte stelle eine Frage.") - if len(body.frage) > 800: - raise HTTPException(400, "Frage zu lang (max. 800 Zeichen).") - - with db() as conn: - _check_dog_access(conn, dog_id, user["id"]) - - dog_name = body.dog_name or "unbekannt" - rasse = body.rasse or "unbekannt" - alter = body.alter or "unbekannt" - gewicht = f"{body.gewicht} kg" if body.gewicht else "unbekannt" - aktiv_str = "aktiv" if body.aktiv else "normal aktiv" - - system = ( - "Du bist Ernährungsberater für Hunde. " - "Antworte immer auf Deutsch, kurz und praktisch. " - "Keine unnötigen Füllsätze. " - "Weise bei ernsthaften Gesundheitsfragen immer auf den Tierarzt hin. " - "Stelle keine medizinischen Diagnosen." - ) - - prompt = ( - f"Hund: {dog_name}, Rasse: {rasse}, Alter: {alter}, " - f"Gewicht: {gewicht}, Aktivität: {aktiv_str}.\n\n" - f"Frage: {body.frage.strip()}\n\n" - "Antworte konkret und praktisch, maximal 200 Wörter." - ) - - try: - antwort = await ki_module.complete( - prompt=prompt, - system=system, - max_tokens=500, - requires_premium=False, - user_id=user["id"], - ) - return {"antwort": antwort} - except ki_module.KIUnavailableError as e: - raise HTTPException(503, str(e)) - except Exception: - raise HTTPException(500, "KI momentan nicht verfügbar.") diff --git a/backend/routes/osm.py b/backend/routes/osm.py index de4cb45..e08742b 100644 --- a/backend/routes/osm.py +++ b/backend/routes/osm.py @@ -279,9 +279,8 @@ class UserPoiIn(BaseModel): ALLOWED_TYPES = { 'waste_basket', 'drinking_water', 'dog_park', - 'giftkoeder', # Giftköder (exklusiv, kein Kombi) + 'giftkoeder', # Giftköder-Meldung (Community-Pin mit Radius) 'kotbeutel', # Kotbeutelspender - 'bank', # Sitzbank 'gefahr', # Allgemeine Gefahr / Hinweis 'parkplatz', # Hundefreundlicher Parkplatz 'treffpunkt', # Treffpunkt für Hundehalter @@ -290,8 +289,7 @@ ALLOWED_TYPES = { @router.post('/user-poi') async def add_user_poi(body: UserPoiIn, user = Depends(get_current_user)): - types = [t.strip() for t in body.type.split(',') if t.strip()] - if not types or any(t not in ALLOWED_TYPES for t in types): + if body.type not in ALLOWED_TYPES: raise HTTPException(400, 'Ungültiger Typ') with db() as conn: row = conn.execute(""" diff --git a/backend/routes/profile.py b/backend/routes/profile.py index 9762f95..08f403a 100644 --- a/backend/routes/profile.py +++ b/backend/routes/profile.py @@ -26,7 +26,6 @@ class ProfileUpdate(BaseModel): social_link: Optional[str] = None profil_sichtbarkeit: Optional[str] = None notes_ki_enabled: Optional[int] = None - gassi_stunde_push: Optional[int] = None def _load_user(user_id: int) -> dict: @@ -114,28 +113,3 @@ async def upload_avatar( ) return {"avatar_url": avatar_url} - - -# ---------------------------------------------------------- -# GET /profile/world-config — Welten-Chip-Konfiguration laden -# PUT /profile/world-config — Welten-Chip-Konfiguration speichern -# ---------------------------------------------------------- -import json as _json - -@router.get('/world-config') -async def get_world_config(user=Depends(get_current_user)): - with db() as conn: - row = conn.execute("SELECT world_config FROM users WHERE id=?", (user['id'],)).fetchone() - cfg = row['world_config'] if row and row['world_config'] else None - return {"config": _json.loads(cfg) if cfg else None} - - -class WorldConfigIn(BaseModel): - config: dict - -@router.put('/world-config') -async def put_world_config(body: WorldConfigIn, user=Depends(get_current_user)): - with db() as conn: - conn.execute("UPDATE users SET world_config=? WHERE id=?", - (_json.dumps(body.config), user['id'])) - return {"status": "ok"} diff --git a/backend/routes/tieraerzte.py b/backend/routes/tieraerzte.py index 8448478..48287f9 100644 --- a/backend/routes/tieraerzte.py +++ b/backend/routes/tieraerzte.py @@ -27,14 +27,6 @@ class TierarztCreate(BaseModel): osm_id: Optional[str] = None -class BewertungCreate(BaseModel): - gesamt: int - wartezeit: Optional[int] = None - freundlichkeit: Optional[int] = None - kompetenz: Optional[int] = None - text: Optional[str] = None - - class TierarztUpdate(BaseModel): name: Optional[str] = None strasse: Optional[str] = None @@ -228,109 +220,3 @@ async def update_tierarzt(tierarzt_id: int, data: TierarztUpdate, ) row = conn.execute("SELECT * FROM tieraerzte WHERE id=?", (tierarzt_id,)).fetchone() return dict(row) - - -# ------------------------------------------------------------------ -# BEWERTUNGEN -# ------------------------------------------------------------------ - -def _refresh_vet_rating(conn, tierarzt_id: int): - """Aktualisiert avg_rating und anz_bewertungen in tieraerzte.""" - row = conn.execute( - """SELECT COUNT(*) AS n, AVG(CAST(gesamt AS REAL)) AS avg - FROM tierarzt_bewertungen WHERE tierarzt_id=?""", - (tierarzt_id,) - ).fetchone() - n = row["n"] or 0 - avg = row["avg"] or 0.0 - conn.execute( - "UPDATE tieraerzte SET avg_rating=?, anz_bewertungen=? WHERE id=?", - (round(avg, 1), n, tierarzt_id) - ) - - -@router.post("/{tierarzt_id}/bewertung", status_code=201) -async def create_bewertung(tierarzt_id: int, data: BewertungCreate, - user=Depends(get_current_user)): - """Bewertung abgeben (1×pro User+Tierarzt, UPSERT).""" - if not (1 <= data.gesamt <= 5): - raise HTTPException(400, "Gesamtbewertung muss zwischen 1 und 5 liegen.") - for field in ("wartezeit", "freundlichkeit", "kompetenz"): - val = getattr(data, field) - if val is not None and not (1 <= val <= 5): - raise HTTPException(400, f"{field} muss zwischen 1 und 5 liegen.") - - text = (data.text or "").strip()[:500] or None - - with db() as conn: - vet = conn.execute("SELECT id FROM tieraerzte WHERE id=?", (tierarzt_id,)).fetchone() - if not vet: - raise HTTPException(404, "Tierarzt nicht gefunden.") - - conn.execute( - """INSERT INTO tierarzt_bewertungen - (tierarzt_id, user_id, gesamt, wartezeit, freundlichkeit, kompetenz, text) - VALUES (?,?,?,?,?,?,?) - ON CONFLICT(tierarzt_id, user_id) DO UPDATE SET - gesamt=excluded.gesamt, - wartezeit=excluded.wartezeit, - freundlichkeit=excluded.freundlichkeit, - kompetenz=excluded.kompetenz, - text=excluded.text, - created_at=datetime('now')""", - (tierarzt_id, user["id"], data.gesamt, data.wartezeit, - data.freundlichkeit, data.kompetenz, text) - ) - _refresh_vet_rating(conn, tierarzt_id) - row = conn.execute( - "SELECT * FROM tieraerzte WHERE id=?", (tierarzt_id,) - ).fetchone() - return dict(row) - - -@router.get("/{tierarzt_id}/bewertungen") -async def list_bewertungen(tierarzt_id: int): - """Alle Bewertungen für einen Tierarzt (public). Gibt Zusammenfassung + letzte 5 Texte.""" - with db() as conn: - vet = conn.execute( - "SELECT id, avg_rating, anz_bewertungen FROM tieraerzte WHERE id=?", - (tierarzt_id,) - ).fetchone() - if not vet: - raise HTTPException(404, "Tierarzt nicht gefunden.") - - # Stern-Verteilung - verteilung = {} - for star in range(1, 6): - r = conn.execute( - "SELECT COUNT(*) AS n FROM tierarzt_bewertungen WHERE tierarzt_id=? AND gesamt=?", - (tierarzt_id, star) - ).fetchone() - verteilung[str(star)] = r["n"] - - # Letzte 5 Kommentare - kommentare = conn.execute( - """SELECT gesamt, wartezeit, freundlichkeit, kompetenz, text, created_at - FROM tierarzt_bewertungen - WHERE tierarzt_id=? AND text IS NOT NULL AND text != '' - ORDER BY created_at DESC LIMIT 5""", - (tierarzt_id,) - ).fetchall() - - return { - "avg_rating": vet["avg_rating"] or 0, - "anz_bewertungen": vet["anz_bewertungen"] or 0, - "verteilung": verteilung, - "kommentare": [dict(k) for k in kommentare], - } - - -@router.get("/{tierarzt_id}/meine-bewertung") -async def get_meine_bewertung(tierarzt_id: int, user=Depends(get_current_user)): - """Eigene Bewertung für einen Tierarzt (oder null).""" - with db() as conn: - row = conn.execute( - "SELECT * FROM tierarzt_bewertungen WHERE tierarzt_id=? AND user_id=?", - (tierarzt_id, user["id"]) - ).fetchone() - return dict(row) if row else None diff --git a/backend/routes/weather.py b/backend/routes/weather.py index ba45306..fced719 100644 --- a/backend/routes/weather.py +++ b/backend/routes/weather.py @@ -3,11 +3,9 @@ BAN YARO — Wetter-API GET /api/weather?lat=&lon= → aktuelles Wetter + Zecken-Warnung für Nutzerstandort """ -import json from fastapi import APIRouter, Query, HTTPException, Depends import weather as weather_module from auth import get_current_user -from database import db router = APIRouter() @@ -33,57 +31,3 @@ async def get_weather_forecast( return await weather_module.get_forecast(lat, lon) except Exception as exc: raise HTTPException(503, f'Wettervorhersage nicht verfügbar: {exc}') - - -@router.get('/records') -async def weather_records(user=Depends(get_current_user)): - """Persönliche Wetterrekorde aus diary-Einträgen mit weather_json.""" - uid = user["id"] - with db() as conn: - 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 - ORDER BY d.datum ASC - """, (uid,)).fetchall() - - if not rows: - return {"records": None} - - entries = [] - for r in rows: - try: - w = json.loads(r["weather_json"]) - entries.append({ - "datum": r["datum"], - "titel": r["titel"], - "temp_c": w.get("temp_c"), - "wind_kmh": w.get("wind_kmh"), - "precip_prob": w.get("precip_prob"), - "desc": w.get("desc", ""), - "weathercode": w.get("weathercode"), - }) - except Exception: - pass - - if not entries: - return {"records": None} - - temps = [e for e in entries if e["temp_c"] is not None] - winds = [e for e in entries if e["wind_kmh"] is not None] - - records = {} - if temps: - kaeltester = min(temps, key=lambda e: e["temp_c"]) - heissester = max(temps, key=lambda e: e["temp_c"]) - records["kaeltester"] = kaeltester - records["heissester"] = heissester - if winds: - stuermischster = max(winds, key=lambda e: e["wind_kmh"]) - records["stuermischster"] = stuermischster - - regen_count = sum(1 for e in entries if (e.get("precip_prob") or 0) > 60) - records["regen_eintraege"] = regen_count - records["gesamt_eintraege"] = len(entries) - - return {"records": records} diff --git a/backend/routes/widget.py b/backend/routes/widget.py index 4af2473..f5cc940 100644 --- a/backend/routes/widget.py +++ b/backend/routes/widget.py @@ -1,33 +1,13 @@ -"""BAN YARO — Widget-Snapshot + Tagesspruch Endpoints""" +"""BAN YARO — Widget-Snapshot Endpoint""" import json, random -from datetime import date -from fastapi import APIRouter, Depends, Query -from typing import Optional +from fastapi import APIRouter, Depends from database import db from auth import get_current_user router = APIRouter() -@router.get("/quote") -async def daily_quote(kategorie: Optional[str] = Query(None)): - """Liefert einen deterministischen Tagesspruch (wechselt täglich).""" - day_num = (date.today() - date(2026, 1, 1)).days - with db() as conn: - if kategorie: - rows = conn.execute( - "SELECT id, text, autor, kategorie FROM daily_quotes WHERE kategorie=?", - (kategorie,) - ).fetchall() - else: - rows = conn.execute("SELECT id, text, autor, kategorie FROM daily_quotes").fetchall() - if not rows: - return {"quote": None} - q = rows[day_num % len(rows)] - return {"quote": dict(q)} - - @router.get("/snapshot") async def widget_snapshot(user=Depends(get_current_user)): """Liefert kompakte Widget-Daten: Hund, nächste Erinnerung, zufälliges Tagebuchbild.""" diff --git a/backend/routes/wiki.py b/backend/routes/wiki.py index 56df55d..45f5bfb 100644 --- a/backend/routes/wiki.py +++ b/backend/routes/wiki.py @@ -414,7 +414,7 @@ async def review_submission(sub_id: int, data: ReviewModel, user=Depends(get_cur raise HTTPException(404, "Einreichung nicht gefunden.") rasse = conn.execute( - "SELECT id, external_id, slug, foto_url FROM wiki_rassen WHERE id=?", + "SELECT id, external_id, slug FROM wiki_rassen WHERE id=?", (sub["rasse_id"],) ).fetchone() diff --git a/backend/scheduler.py b/backend/scheduler.py index 4d1dbff..4aeb89a 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -156,32 +156,8 @@ def start(): replace_existing=True, misfire_grace_time=3600, ) - # Täglich 07:00 Uhr — Goldene Gassi-Stunde - _scheduler.add_job( - _job_golden_gassi_hour, - CronTrigger(hour=7, minute=0), - id="golden_gassi_hour", - replace_existing=True, - misfire_grace_time=3600, - ) - # Täglich 09:00 Uhr — Jahrestags-Erinnerungen (Tagebuch-Einträge von heute vor X Jahren) - _scheduler.add_job( - _job_anniversary_reminders, - CronTrigger(hour=9, minute=0), - id="anniversary_reminders", - replace_existing=True, - misfire_grace_time=3600, - ) - # 1. des Monats 10:00 — Monatlicher Rückblick per Push - _scheduler.add_job( - _job_monthly_recap, - CronTrigger(day=1, hour=10, minute=0), - id="monthly_recap", - replace_existing=True, - misfire_grace_time=3600, - ) _scheduler.start() - logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00, Goldene-Gassi-Stunde 07:00, Jahrestags-Erinnerungen 09:00, Monatlicher-Rückblick 1. des Monats 10:00. OSM-Cache: on-demand (kein Prewarm).") + logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00. OSM-Cache: on-demand (kein Prewarm).") def stop(): @@ -905,9 +881,6 @@ async def _job_status_report(): "quarterly_report": "Quartalsbericht (1. Feb/Mai/Aug/Nov)", "streak_reminder": "Streak-Erinnerung (täglich 19:00)", "recall_check": "Tierfutter-Rückrufe (RASFF, täglich 08:00)", - "golden_gassi_hour": "Goldene Gassi-Stunde (täglich 07:00)", - "anniversary_reminders": "Jahrestags-Erinnerungen (täglich 09:00)", - "monthly_recap": "Monatlicher Rückblick (1. des Monats 10:00)", } job_rows_html = "" job_rows_txt = "" @@ -1315,329 +1288,3 @@ async def _job_recurring_expenses(): except Exception as e: logger.error(f"Daueraufträge-Job Fehler: {e}") _log_job("recurring_expenses", "error", str(e)) - - -# ------------------------------------------------------------------ -# JOB: Goldene Gassi-Stunde (täglich 07:00 Uhr) -# ------------------------------------------------------------------ -async def _job_golden_gassi_hour(): - """ - Berechnet für jeden User mit aktivierter Einstellung (gassi_stunde_push=1) - das beste 2h-Wetterfenster des Tages und schickt eine Push-Notification. - - Score-Logik pro Stunde (max. 10 Punkte): - - Temperatur 10–20°C → +3 - - Temperatur 5–10°C → +1 - - Niederschlagswahrsch. <20% → +3, <40% → +1 - - Windgeschwindigkeit <20 km/h → +2, <30 km/h → +1 - - Stunden 07–19 Uhr (Tageslicht) → +2 - Bestes fortlaufendes 2h-Fenster (Summe zweier aufeinanderfolgender Stunden). - """ - import httpx - from datetime import date as _date - - logger.info("Goldene-Gassi-Stunde Job läuft") - - # Alle User mit aktivierter Einstellung + mindestens einer Push-Subscription - with db() as conn: - users = conn.execute(""" - SELECT DISTINCT u.id AS user_id, - ps.last_lat, ps.last_lon - FROM users u - JOIN push_subscriptions ps ON ps.user_id = u.id - WHERE u.gassi_stunde_push = 1 - """).fetchall() - - users = [dict(u) for u in users] - logger.info(f"Goldene-Gassi-Stunde: {len(users)} User mit aktivierter Einstellung.") - - if not users: - _log_job("golden_gassi_hour", "ok", "0 User mit Einstellung aktiv") - return - - sent_total = 0 - - for u in users: - lat = u["last_lat"] or 48.1351 # Fallback: München - lon = u["last_lon"] or 11.5820 - - try: - hourly = await _fetch_hourly_weather(lat, lon) - except Exception as e: - logger.warning(f"Goldene-Gassi-Stunde: Wetter-Fehler für user {u['user_id']}: {e}") - continue - - if not hourly: - continue - - best_start, best_score, best_temp, best_wind = _find_best_gassi_window(hourly) - - if best_score < 3: - # Heute kein gutes Wetterfenster → kein Push - logger.info(f"Goldene-Gassi-Stunde: user {u['user_id']} — kein gutes Fenster (score={best_score})") - continue - - hour_end = (best_start + 2) % 24 - temp_str = f"{best_temp:.0f}°C" if best_temp is not None else "–" - wind_str = "Kaum Wind" if (best_wind is not None and best_wind < 20) else ( - f"{best_wind:.0f} km/h Wind" if best_wind is not None else "") - - body_parts = [f"Bestes Wetter zwischen {best_start:02d}:00–{hour_end:02d}:00 Uhr", - f"· {temp_str}"] - if wind_str: - body_parts.append(f"· {wind_str}") - - sent = send_push_to_user(u["user_id"], { - "type": "golden_gassi_hour", - "title": "☀️ Goldene Gassi-Stunde heute!", - "body": " ".join(body_parts), - "data": {"page": "wetter"}, - "tag": f"gassi-{_date.today()}", - }) - sent_total += sent - logger.info(f"Goldene-Gassi-Stunde: user {u['user_id']} → {best_start:02d}:00 (score={best_score}, {temp_str}) — Push: {sent}") - - logger.info(f"Goldene-Gassi-Stunde Job fertig — {len(users)} User, {sent_total} Push gesendet.") - _log_job("golden_gassi_hour", "ok", f"{sent_total} Push an {len(users)} User") - - -# ------------------------------------------------------------------ -# JOB: Jahrestags-Erinnerungen (täglich 09:00) -# ------------------------------------------------------------------ -async def _job_anniversary_reminders(): - """Prüft ob heute ein Jahrestag für diary-Einträge vorliegt und sendet Push.""" - today = datetime.now(tz=_TZ) - today_md = today.strftime('%m-%d') # Monat-Tag ohne Jahr - - logger.info(f"Jahrestags-Erinnerungen Job läuft für {today_md}") - - with db() as conn: - entries = conn.execute(""" - SELECT d.id, d.titel, d.datum, d.user_id, d.dog_id, - (SELECT dm.url FROM diary_media dm - WHERE dm.diary_id=d.id LIMIT 1) AS foto_url - FROM diary d - WHERE strftime('%m-%d', d.datum) = ? - AND d.datum < date('now') - AND d.titel IS NOT NULL - AND d.is_milestone = 0 - """, (today_md,)).fetchall() - - sent_total = 0 - for e in entries: - try: - jahre = today.year - int(e['datum'][:4]) - if jahre < 1: - continue - jahre_label = f"{jahre} Jahr" if jahre == 1 else f"{jahre} Jahren" - send_push_to_user(e['user_id'], { - 'type': 'anniversary_reminder', - 'title': f'📅 Vor {jahre_label}: {(e["titel"] or "")[:40]}', - 'body': 'Erinnerung an diesen besonderen Tag mit deinem Hund', - 'data': {'page': 'diary'}, - 'tag': f'anniversary-{e["id"]}-{today.year}', - }) - sent_total += 1 - except Exception as ex: - logger.warning(f"Jahrestag-Reminder: Fehler für Eintrag {e['id']}: {ex}") - - logger.info(f"Jahrestags-Erinnerungen Job fertig — {len(entries)} Einträge geprüft, {sent_total} Push gesendet.") - _log_job("anniversary_reminders", "ok", f"{sent_total} Push von {len(entries)} Einträgen") - - -# ------------------------------------------------------------------ -# JOB: Monatlicher Rückblick (1. des Monats 10:00) -# ------------------------------------------------------------------ -async def _job_monthly_recap(): - """Sendet jedem User am 1. des Monats einen Rückblick des Vormonats.""" - today = datetime.now(tz=_TZ) - first_this = today.replace(day=1) - last_month_end = first_this - timedelta(days=1) - last_month_start = last_month_end.replace(day=1) - year_str = last_month_start.strftime('%Y') - month_str = last_month_start.strftime('%m') - month_label = last_month_start.strftime('%B %Y') - - logger.info(f"Monatlicher Rückblick Job läuft für {month_label}") - - with db() as conn: - # Alle User mit mindestens einem Hund - users = conn.execute( - "SELECT DISTINCT user_id FROM dogs" - ).fetchall() - - sent_total = 0 - for u in users: - user_id = u["user_id"] - try: - with db() as conn: - # Hunde des Users - dog_rows = conn.execute( - "SELECT id, name FROM dogs WHERE user_id=?", (user_id,) - ).fetchall() - if not dog_rows: - continue - - dog_ids = [d["id"] for d in dog_rows] - placeholders = ','.join('?' * len(dog_ids)) - - # km (Routen des Users im Vormonat) - km_row = conn.execute( - "SELECT ROUND(COALESCE(SUM(distanz_km),0),1) AS km FROM routes " - "WHERE user_id=? AND strftime('%Y',created_at)=? AND strftime('%m',created_at)=?", - (user_id, year_str, month_str) - ).fetchone() - gesamt_km = km_row["km"] or 0.0 - - # Tagebucheinträge - eintraege = conn.execute( - f"SELECT COUNT(*) AS n FROM diary " - f"WHERE dog_id IN ({placeholders}) AND strftime('%Y',datum)=? AND strftime('%m',datum)=?", - (*dog_ids, year_str, month_str) - ).fetchone()["n"] - - # Training-Sessions - training = conn.execute( - f"SELECT COUNT(*) AS n FROM training_sessions " - f"WHERE dog_id IN ({placeholders}) AND strftime('%Y',created_at)=? AND strftime('%m',created_at)=?", - (*dog_ids, year_str, month_str) - ).fetchone()["n"] - - # Lieblingsfoto (erstes Foto im Vormonat) - foto_row = conn.execute( - f"SELECT dm.url FROM diary_media dm " - f"JOIN diary d ON d.id=dm.diary_id " - f"WHERE d.dog_id IN ({placeholders}) AND dm.media_type='image' " - f"AND strftime('%Y',d.datum)=? AND strftime('%m',d.datum)=? " - f"ORDER BY d.datum ASC LIMIT 1", - (*dog_ids, year_str, month_str) - ).fetchone() - foto_url = foto_row["url"] if foto_row else None - - # Nur senden wenn mindestens eine Aktivität vorhanden - if eintraege == 0 and training == 0 and gesamt_km == 0: - continue - - dog_name = dog_rows[0]["name"] - parts = [] - if gesamt_km > 0: - parts.append(f"{gesamt_km} km gelaufen") - if eintraege > 0: - parts.append(f"{eintraege} Tagebucheintr{'ä' if True else 'a'}ge") - if training > 0: - parts.append(f"{training} Training-Sessions") - - body_text = " · ".join(parts) - - send_push_to_user(user_id, { - 'type': 'monthly_recap', - 'title': f'📅 {month_label}: Rückblick für {dog_name}', - 'body': body_text, - 'data': {'page': 'diary'}, - 'tag': f'monthly-recap-{year_str}-{month_str}', - }) - sent_total += 1 - except Exception as ex: - logger.error(f"Monatlicher Rückblick: Fehler für user {user_id}: {ex}") - - logger.info(f"Monatlicher Rückblick Job fertig — {len(users)} User geprüft, {sent_total} Push gesendet.") - _log_job("monthly_recap", "ok", f"{sent_total} Push für {month_label}") - - -async def _fetch_hourly_weather(lat: float, lon: float) -> list[dict]: - """Holt stündliche Wetterdaten für heute von Open-Meteo.""" - import httpx - from datetime import date as _date - - today = _date.today().isoformat() - url = ( - "https://api.open-meteo.com/v1/forecast" - f"?latitude={lat}&longitude={lon}" - "&hourly=temperature_2m,precipitation_probability,windspeed_10m" - "&timezone=Europe%2FBerlin&forecast_days=1" - ) - async with httpx.AsyncClient(timeout=8.0) as client: - resp = await client.get(url) - resp.raise_for_status() - raw = resp.json() - - hourly = raw.get("hourly", {}) - times = hourly.get("time", []) - temps = hourly.get("temperature_2m", []) - precips = hourly.get("precipitation_probability", []) - winds = hourly.get("windspeed_10m", []) - - result = [] - for i, ts in enumerate(times): - if not ts.startswith(today): - continue - hour = int(ts[11:13]) - result.append({ - "hour": hour, - "temp": temps[i] if i < len(temps) else None, - "precip": precips[i] if i < len(precips) else None, - "wind": winds[i] if i < len(winds) else None, - }) - return result - - -def _score_hour(h: dict) -> int: - """Berechnet Gassi-Score für eine einzelne Stunde (0–10 Punkte).""" - score = 0 - temp = h.get("temp") - precip = h.get("precip") - wind = h.get("wind") - hour = h.get("hour", 12) - - # Temperatur - if temp is not None: - if 10 <= temp <= 20: - score += 3 - elif 5 <= temp < 10 or 20 < temp <= 25: - score += 1 - - # Niederschlagswahrscheinlichkeit - if precip is not None: - if precip < 20: - score += 3 - elif precip < 40: - score += 1 - - # Wind - if wind is not None: - if wind < 20: - score += 2 - elif wind < 30: - score += 1 - - # Tageslicht (07–19 Uhr) - if 7 <= hour <= 19: - score += 2 - - return score - - -def _find_best_gassi_window(hourly: list[dict]) -> tuple[int, int, float | None, float | None]: - """ - Findet das beste aufeinanderfolgende 2h-Fenster. - Gibt (start_hour, total_score, avg_temp, avg_wind) zurück. - """ - best_start = 8 - best_score = -1 - best_temp = None - best_wind = None - - for i in range(len(hourly) - 1): - h1 = hourly[i] - h2 = hourly[i + 1] - combined = _score_hour(h1) + _score_hour(h2) - if combined > best_score: - best_score = combined - best_start = h1["hour"] - # Durchschnittswerte für Anzeige - temps = [x for x in [h1.get("temp"), h2.get("temp")] if x is not None] - winds = [x for x in [h1.get("wind"), h2.get("wind")] if x is not None] - best_temp = sum(temps) / len(temps) if temps else None - best_wind = sum(winds) / len(winds) if winds else None - - return best_start, best_score, best_temp, best_wind diff --git a/backend/static/css/components.css b/backend/static/css/components.css index c8de6a6..27cf0d9 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -5571,139 +5571,6 @@ html.modal-open { border-radius: 0; } -/* ── Wiki Gallery ────────────────────────────────────────── */ -.wiki-gallery-wrap { - position: relative; - margin-bottom: var(--space-3); -} -.wiki-gallery-main { - width: 100%; - height: 240px; - object-fit: cover; - object-position: center top; - border-radius: var(--radius-lg); - display: block; -} -.wiki-gallery-strip { - display: flex; - gap: var(--space-2); - overflow-x: auto; - padding: var(--space-2) 0 0; - scrollbar-width: none; -} -.wiki-gallery-strip::-webkit-scrollbar { display: none; } -.wiki-gallery-thumb { - flex-shrink: 0; - width: 64px; height: 64px; - border-radius: var(--radius-md); - overflow: hidden; - border: 2px solid transparent; - padding: 0; - background: none; - cursor: pointer; - position: relative; - transition: border-color .15s; -} -.wiki-gallery-thumb.active { border-color: var(--c-primary); } -.wiki-gallery-thumb img { - width: 100%; height: 100%; object-fit: cover; -} -.wiki-gallery-thumb-label { - position: absolute; - bottom: 0; left: 0; right: 0; - background: rgba(0,0,0,.55); - color: #fff; - font-size: 8px; - padding: 2px 4px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} -.wiki-gallery-expand { - position: absolute; - top: var(--space-2); - right: var(--space-2); - width: 34px; height: 34px; - border-radius: 50%; - background: rgba(0,0,0,.45); - border: none; - color: #fff; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - backdrop-filter: blur(4px); - transition: background .15s; -} -.wiki-gallery-expand:hover { background: rgba(0,0,0,.65); } - -/* ── Wiki Lightbox ───────────────────────────────────────── */ -#wiki-lightbox { - position: fixed; - inset: 0; - z-index: 2000; - display: flex; - align-items: center; - justify-content: center; -} -.wlb-backdrop { - position: absolute; - inset: 0; - background: rgba(0,0,0,.88); - backdrop-filter: blur(6px); -} -.wlb-content { - position: relative; - z-index: 1; - display: flex; - flex-direction: column; - align-items: center; - max-width: min(92vw, 680px); - width: 100%; - gap: var(--space-2); -} -.wlb-img { - width: 100%; - max-height: 72vh; - object-fit: contain; - border-radius: var(--radius-lg); -} -.wlb-close { - position: absolute; - top: -44px; - right: 0; - background: rgba(255,255,255,.12); - border: none; - color: #fff; - width: 36px; height: 36px; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; -} -.wlb-prev, .wlb-next { - position: absolute; - top: 50%; - transform: translateY(-50%); - background: rgba(255,255,255,.12); - border: none; - color: #fff; - width: 40px; height: 40px; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.4rem; - transition: background .15s; -} -.wlb-prev { left: -48px; } -.wlb-next { right: -48px; } -.wlb-prev:hover, .wlb-next:hover { background: rgba(255,255,255,.25); } -.wlb-caption { color: rgba(255,255,255,.75); font-size: var(--text-sm); } -.wlb-counter { color: rgba(255,255,255,.45); font-size: var(--text-xs); } - /* Steckbrief-Grid */ .wiki-steckbrief-grid { display: grid; @@ -6683,97 +6550,6 @@ html.modal-open { /* ============================================================ HELP TOOLTIP ============================================================ */ -/* ============================================================ - PAGE INFO — generische Seiten-Hilfe (UI.pageInfo) - ============================================================ */ -.pinfo-trigger-inline { - width: 26px; height: 26px; - border-radius: 50%; - background: var(--c-surface-2); - border: 1px solid var(--c-border-light); - color: var(--c-text-secondary); - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - transition: background .15s, color .15s; -} -.pinfo-trigger-inline:hover { background: var(--c-primary-subtle, rgba(196,132,58,.1)); color: var(--c-primary); } - -.pinfo-banner { - margin: var(--space-3) var(--space-4) 0; - padding: var(--space-3) var(--space-4); - border-radius: var(--radius-lg); - background: var(--c-surface-2); - border-left: 3px solid var(--c-primary); - font-size: var(--text-sm); -} -.pinfo-banner-head { - display: flex; - align-items: center; - gap: var(--space-2); - margin-bottom: var(--space-2); -} -.pinfo-banner-icon { color: var(--c-primary); flex-shrink: 0; } -.pinfo-banner-title { - flex: 1; - font-weight: var(--weight-semibold); - color: var(--c-text); -} -.pinfo-banner-close { - background: none; border: none; cursor: pointer; - color: var(--c-text-muted); padding: 2px; -} -.pinfo-banner-intro { color: var(--c-text-secondary); margin-bottom: var(--space-2); line-height: 1.5; } -.pinfo-banner-more { - background: none; border: none; cursor: pointer; - color: var(--c-primary); - font-size: var(--text-xs); - font-weight: var(--weight-medium); - padding: 0; - display: flex; - align-items: center; - gap: 4px; - margin-top: var(--space-2); -} - -/* MODAL BODY */ -.pinfo-modal { display: flex; flex-direction: column; gap: var(--space-3); } -.pinfo-intro { color: var(--c-text-secondary); line-height: 1.6; margin: 0; } -.pinfo-steps { display: flex; flex-direction: column; gap: var(--space-3); } -.pinfo-steps--compact { gap: var(--space-2); margin-top: var(--space-2); } -.pinfo-step { - display: flex; - gap: var(--space-3); - align-items: flex-start; -} -.pinfo-step-icon { - width: 32px; height: 32px; - border-radius: var(--radius-md); - background: var(--c-primary-subtle, rgba(196,132,58,.12)); - color: var(--c-primary); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} -.pinfo-step-title { font-weight: var(--weight-semibold); color: var(--c-text); font-size: var(--text-sm); margin-bottom: 2px; } -.pinfo-step-text { color: var(--c-text-secondary); font-size: var(--text-sm); line-height: 1.5; } -.pinfo-tip { - display: flex; - gap: var(--space-2); - align-items: flex-start; - padding: var(--space-3); - background: rgba(196,132,58,.08); - border-radius: var(--radius-md); - color: var(--c-text-secondary); - font-size: var(--text-sm); - line-height: 1.5; -} -.pinfo-tip .ph-icon { color: var(--c-primary); flex-shrink: 0; margin-top: 1px; } - - .by-help-btn { display: inline-flex; align-items: center; @@ -7178,7 +6954,7 @@ svg.empty-state-icon { /* FAB */ .exp-fab { position: fixed; - bottom: calc(var(--nav-bottom-height) + var(--safe-bottom) + var(--space-2)); + bottom: calc(var(--nav-height, 64px) + var(--space-4)); right: var(--space-4); z-index: 100; width: 52px; @@ -7824,28 +7600,10 @@ svg.empty-state-icon { .wlabel.active { opacity: 1; } @media (min-width: 768px) { - #world-labels { - gap: 40px; - top: calc(env(safe-area-inset-top, 0px) + 18px); - } - .wlabel { - font-size: 13px; - letter-spacing: 0.18em; - opacity: 0.55; - padding: 6px 14px; - border-radius: 20px; - text-shadow: 0 1px 6px rgba(0,0,0,0.7); - transition: opacity 0.18s, background 0.18s; - } - .wlabel:hover { - opacity: 0.85; - background: rgba(255, 255, 255, 0.12); - } - .wlabel.active { - opacity: 1; - background: rgba(255, 255, 255, 0.18); - text-shadow: 0 1px 8px rgba(0,0,0,0.5); - } + #world-labels { gap: 48px; font-size: 11px; } + .wlabel { opacity: 0.5; padding: 4px 10px; border-radius: 8px; } + .wlabel:hover { opacity: 0.8; background: rgba(255,255,255,0.08); } + .wlabel.active { opacity: 1; background: rgba(255,255,255,0.12); } } /* Settings-Button */ diff --git a/backend/static/index.html b/backend/static/index.html index 77f0433..cb75a8f 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -9,6 +9,7 @@ + @@ -75,7 +76,6 @@ - @@ -93,9 +93,9 @@ - - - + + + @@ -499,18 +499,6 @@
-
-
-
- -
-
-
- -
-
-
- @@ -551,6 +539,9 @@ HUND WELT +
@@ -574,12 +565,12 @@ - + - + diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 1071fdd..c6b26da 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -212,9 +212,6 @@ const API = (() => { osmNearby(lat, lon) { return get(`/tieraerzte/osm-nearby?lat=${lat}&lon=${lon}`); }, myFavorite() { return get('/tieraerzte/my-favorite'); }, toggleFavorite(id) { return post(`/tieraerzte/${id}/favorite`); }, - bewertungen(id) { return get(`/tieraerzte/${id}/bewertungen`); }, - meineBewertung(id) { return get(`/tieraerzte/${id}/meine-bewertung`); }, - bewertungAbgeben(id, data) { return post(`/tieraerzte/${id}/bewertung`, data); }, }; // ---------------------------------------------------------- diff --git a/backend/static/js/app.js b/backend/static/js/app.js index deb05c5..a248787 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 = '700'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '651'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.3.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; @@ -76,9 +76,6 @@ const App = (() => { adoption: { title: 'Adoption', module: null }, playdate: { title: 'Playdate', module: null, requiresAuth: true }, wetter: { title: 'Wetter', module: null }, - ernaehrung: { title: 'Ernährung', module: null, requiresAuth: true }, - personality: { title: 'Persönlichkeitstest', module: null }, - reise: { title: 'Reise mit Hund', module: null }, }; // ---------------------------------------------------------- diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index 66d9150..a39eccd 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -868,9 +868,9 @@ window.Page_diary = (() => { if (e.weather_json) { try { const w = typeof e.weather_json === 'string' ? JSON.parse(e.weather_json) : e.weather_json; - const temp = w?.temp_c ?? w?.temperature_2m; + const temp = w?.temperature_2m ?? w?.temp_c; if (temp != null) { - metaParts.push(`${_weatherEmoji(w.weathercode ?? w.weather_code, w.is_day)} ${Math.round(temp)}°`); + metaParts.push(`${_weatherEmoji(w.weather_code ?? w.weathercode, w.is_day)} ${Math.round(temp)}°`); } } catch (_) {} } @@ -1073,14 +1073,15 @@ window.Page_diary = (() => { if (entry.weather_json) { try { const w = typeof entry.weather_json === 'string' ? JSON.parse(entry.weather_json) : entry.weather_json; - const temp = w?.temp_c ?? w?.temperature_2m; + const temp = w?.temperature_2m ?? w?.temp_c; if (w && temp != null) { - const wind = w.wind_kmh ?? w.wind_speed_10m; - const precip = w.precip_prob; + const feels = w.apparent_temperature ?? w.feels_like_c; + const wind = w.wind_speed_10m ?? w.wind_kmh; const parts = [ - `${_weatherEmoji(w.weathercode ?? w.weather_code, w.is_day)} ${Math.round(temp)}°C`, - wind != null ? `${Math.round(wind)} km/h Wind` : null, - precip != null ? `${precip}% Regen` : null, + `${_weatherEmoji(w.weather_code ?? w.weathercode, w.is_day)} ${Math.round(temp)}°C`, + feels != null ? `gefühlt ${Math.round(feels)}°` : null, + wind != null ? `💨 ${Math.round(wind)} km/h` : null, + w.relative_humidity_2m != null ? `💧 ${w.relative_humidity_2m}%` : null, ].filter(Boolean).join(' · '); metaItems.push(`${parts}`); } @@ -1727,16 +1728,6 @@ window.Page_diary = (() => { }); await UI.asyncButton(submitBtn, async () => { - // Auto-Wetter: nur bei neuem Eintrag ohne GPS-Standort - let _clientWeather = null; - if (!isEdit && _locLat == null) { - try { - const pos = await API.getLocation(); - const wd = await API.weather.get(pos.lat, pos.lon); - if (wd && wd.temp_c != null) _clientWeather = JSON.stringify(wd); - } catch (_) { /* GPS oder Wetter nicht verfügbar → kein Problem */ } - } - const payload = { datum: fd.datum || null, typ: fd.typ, @@ -1748,7 +1739,6 @@ window.Page_diary = (() => { gps_lon: _locLon, location_name: _locName, client_time: API.clientNow(), - weather_json: _clientWeather, }; async function _uploadNewFiles(entryId) { diff --git a/backend/static/js/pages/dog-profile.js b/backend/static/js/pages/dog-profile.js index 09b729a..82a2e8a 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -195,27 +195,9 @@ window.Page_dog_profile = (() => { Hundepass ` : ''} - ${!dog.is_guest ? `` : ''} ${!dog.is_guest ? `` : ''} - ${!dog.is_guest ? `` : ''} - ${!dog.is_guest ? `` : ''} - ${!dog.is_guest ? `` : ''}
@@ -282,22 +264,6 @@ window.Page_dog_profile = (() => { _showPassportModal(dog); }); - document.getElementById('dp-vcard-btn')?.addEventListener('click', () => { - _showVcardModal(dog); - }); - - document.getElementById('dp-wrapped-btn')?.addEventListener('click', () => { - _showWrappedModal(dog); - }); - - document.getElementById('dp-buch-btn')?.addEventListener('click', () => { - _showBuchModal(dog); - }); - - document.getElementById('dp-timeline-btn')?.addEventListener('click', () => { - _showTimelineModal(dog); - }); - // Edit- und Add-Klicks laufen über Event-Delegation in init() — keine direkten Listener nötig. } @@ -784,138 +750,6 @@ window.Page_dog_profile = (() => { // ---------------------------------------------------------- // TEILEN // ---------------------------------------------------------- - // ---------------------------------------------------------- - // HUNDE-VISITENKARTE MIT QR-CODE - // ---------------------------------------------------------- - function _showVcardModal(dog) { - const passportUrl = `https://banyaro.app/hund/${dog.id}`; - const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=140x140&color=ffffff&bgcolor=1a2035&data=${encodeURIComponent(passportUrl)}`; - - const user = _appState?.user; - const ownerName = user?.name || ''; - const wohnort = user?.wohnort || ''; - - // Alter errechnen - let alterStr = ''; - if (dog.geburtstag) { - const birth = new Date(dog.geburtstag + 'T00:00:00'); - const now = new Date(); - const years = now.getFullYear() - birth.getFullYear() - - (now < new Date(now.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0); - alterStr = years < 1 - ? `${Math.max(1, Math.round((now - birth) / (30.5 * 86400000)))} Monate` - : years === 1 ? '1 Jahr' : `${years} Jahre`; - } - - const metaLine = [dog.rasse, alterStr].filter(Boolean).join(' · '); - - const cardHtml = ` -
- - -
-
- - -
- ${dog.foto_url - ? `` - : `
🐾
`} -
-
${_esc(dog.name)}
- ${metaLine ? `
${_esc(metaLine)}
` : ''} - ${wohnort ? `
📍 ${_esc(wohnort)}
` : ''} -
-
- - -
- - -
-
- ${ownerName ? `
Besitzer
-
${_esc(ownerName)}
` : ''} -
banyaro.app
-
-
- QR-Code -
Profil öffnen
-
-
-
- `; - - UI.modal.open({ - title: 'Visitenkarte', - body: ` -
${cardHtml}
-

- QR-Code auf NFC-Tag oder Anhänger kleben — jeder kann das Profil von ${_esc(dog.name)} sofort öffnen. -

- `, - footer: ` - - - `, - }); - - // Link kopieren - document.getElementById('dp-vcard-copy-btn')?.addEventListener('click', async () => { - try { - await navigator.clipboard.writeText(passportUrl); - UI.toast.success('Link kopiert!'); - } catch { - const inp = document.createElement('input'); - inp.value = passportUrl; - document.body.appendChild(inp); - inp.select(); - document.execCommand('copy'); - inp.remove(); - UI.toast.success('Link kopiert!'); - } - }); - - // Native Share API - document.getElementById('dp-vcard-share-btn')?.addEventListener('click', async () => { - if (navigator.share) { - try { - await navigator.share({ - title: `${dog.name} auf Ban Yaro`, - text: `Schau dir das Profil von ${dog.name} an!`, - url: passportUrl, - }); - } catch {} - } else { - // Fallback: kopieren - try { - await navigator.clipboard.writeText(passportUrl); - UI.toast.success('Link kopiert!'); - } catch { - UI.toast.error('Teilen nicht verfügbar.'); - } - } - }); - } - async function _showShareModal(dog) { UI.modal.open({ title: `${_esc(dog.name)} teilen`, @@ -1136,23 +970,6 @@ window.Page_dog_profile = (() => { -
- - -
-
@@ -1756,226 +1716,6 @@ window.Page_health = (() => { `; } - // ---------------------------------------------------------- - // PRAXEN — Sterne-Hilfs-Funktionen - // ---------------------------------------------------------- - - /** Rendert 5 Sterne (readonly, filled bis `rating`). */ - function _renderStarsReadonly(rating) { - const full = Math.round(rating); - return Array.from({ length: 5 }, (_, i) => { - const filled = i < full; - return ``; - }).join(''); - } - - /** Rendert 5 klickbare Sterne mit data-val. */ - function _renderStarsInput(name, current) { - return `
- ${Array.from({ length: 5 }, (_, i) => { - const val = i + 1; - const filled = current >= val; - return ``; - }).join('')} -
`; - } - - // ---------------------------------------------------------- - // PRAXEN — Detail-Modal (Bewertungen anzeigen) - // ---------------------------------------------------------- - async function _showPraxisDetail(praxis) { - // Erst mit Lade-Spinner öffnen, dann Daten laden - UI.modal.open({ - title: _esc(praxis.name), - body: `
- -
`, - footer: ` - `, - }); - - document.getElementById('detail-bewerten-btn') - ?.addEventListener('click', () => { UI.modal.close(); _showBewertungModal(praxis); }); - - let data; - try { - data = await API.tieraerzte.bewertungen(praxis.id); - } catch { - UI.modal.open({ title: praxis.name, body: '

Bewertungen konnten nicht geladen werden.

' }); - return; - } - - const { avg_rating, anz_bewertungen, verteilung, kommentare } = data; - - // Balkendiagramm - const balken = [5, 4, 3, 2, 1].map(s => { - const n = verteilung[String(s)] || 0; - const pct = anz_bewertungen > 0 ? Math.round((n / anz_bewertungen) * 100) : 0; - return `
- ${s} - -
-
-
- ${n} -
`; - }).join(''); - - const kommentarHtml = kommentare.length - ? kommentare.map(k => ` -
-
- ${_renderStarsReadonly(k.gesamt)} - - ${k.created_at ? k.created_at.slice(0, 10) : ''} - -
- ${k.wartezeit || k.freundlichkeit || k.kompetenz ? ` -
- ${k.wartezeit ? `Wartezeit: ${_renderStarsReadonly(k.wartezeit)}` : ''} - ${k.freundlichkeit ? `Freundlichkeit: ${_renderStarsReadonly(k.freundlichkeit)}` : ''} - ${k.kompetenz ? `Kompetenz: ${_renderStarsReadonly(k.kompetenz)}` : ''} -
` : ''} -

${_esc(k.text || '')}

-
`).join('') - : `

Noch keine Kommentare.

`; - - const bewBody = anz_bewertungen === 0 - ? `

- Noch keine Bewertungen — sei der Erste! -

` - : ` -
-
-
${avg_rating.toFixed(1)}
-
${_renderStarsReadonly(avg_rating)}
-
${anz_bewertungen} Bewertung${anz_bewertungen !== 1 ? 'en' : ''}
-
-
${balken}
-
-
${kommentarHtml}
`; - - // Modal-Body aktualisieren (ohne Modal neu zu öffnen) - const modalBody = document.querySelector('.modal-body'); - if (modalBody) modalBody.innerHTML = bewBody; - } - - // ---------------------------------------------------------- - // PRAXEN — Bewertungs-Modal - // ---------------------------------------------------------- - async function _showBewertungModal(praxis) { - // Ggf. bestehende Bewertung laden - let existing = null; - try { existing = await API.tieraerzte.meineBewertung(praxis.id); } catch { /* ok */ } - - const cur = existing || {}; - - const body = ` -
-
- - ${_renderStarsInput('gesamt', cur.gesamt || 0)} - -
-
- - ${_renderStarsInput('wartezeit', cur.wartezeit || 0)} - -
-
- - ${_renderStarsInput('freundlichkeit', cur.freundlichkeit || 0)} - -
-
- - ${_renderStarsInput('kompetenz', cur.kompetenz || 0)} - -
-
- - -
max. 500 Zeichen
-
-
`; - - UI.modal.open({ - title: `${_esc(praxis.name)} bewerten`, - body, - footer: ` - - `, - }); - - // Sterne-Interaktion - document.querySelectorAll('.bew-stars').forEach(group => { - const name = group.dataset.name; - const hidden = document.getElementById(`bew-${name}`); - const stars = group.querySelectorAll('.bew-star'); - - const paint = val => { - stars.forEach(s => { - s.style.color = parseInt(s.dataset.val) <= val - ? 'var(--c-warning,#f59e0b)' : 'var(--c-border)'; - }); - }; - - stars.forEach(s => { - s.addEventListener('mouseover', () => paint(parseInt(s.dataset.val))); - s.addEventListener('mouseleave', () => paint(parseInt(hidden.value))); - s.addEventListener('click', () => { - hidden.value = s.dataset.val; - paint(parseInt(s.dataset.val)); - }); - }); - - paint(parseInt(hidden.value)); - }); - - // Submit - document.getElementById('bew-submit-btn').addEventListener('click', async (e) => { - e.preventDefault(); - const form = document.getElementById('bew-form'); - const gesamt = parseInt(document.getElementById('bew-gesamt').value); - if (!gesamt) { UI.toast.error('Bitte vergib mindestens einen Stern für den Gesamteindruck.'); return; } - - const payload = { gesamt }; - const wz = parseInt(document.getElementById('bew-wartezeit').value); - const fr = parseInt(document.getElementById('bew-freundlichkeit').value); - const ko = parseInt(document.getElementById('bew-kompetenz').value); - if (wz) payload.wartezeit = wz; - if (fr) payload.freundlichkeit = fr; - if (ko) payload.kompetenz = ko; - const txt = form.querySelector('textarea[name="text"]').value.trim(); - if (txt) payload.text = txt; - - await UI.asyncButton(document.getElementById('bew-submit-btn'), async () => { - const saved = await API.tieraerzte.bewertungAbgeben(praxis.id, payload); - // _praxen-Cache aktualisieren - _praxen = _praxen.map(p => - p.id === praxis.id - ? { ...p, avg_rating: saved.avg_rating, anz_bewertungen: saved.anz_bewertungen } - : p - ); - UI.modal.close(); - UI.toast.success('Bewertung gespeichert.'); - _renderTab(); - }); - }); - } - // ---------------------------------------------------------- // PRAXEN — Formular (Neu / Bearbeiten) // ---------------------------------------------------------- diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index fd15e1d..ded9a7d 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -838,17 +838,15 @@ window.Page_map = (() => { _tempMarker = null; } - // Einzelne Basis-Typen — Mehrfachauswahl möglich (außer giftkoeder = exklusiv) const PIN_TYPES = [ - { type: 'giftkoeder', icon: '', label: 'Giftköder', color: '#DC2626', exclusive: true }, - { type: 'waste_basket', icon: '', label: 'Mülleimer', color: '#6B7280' }, - { type: 'kotbeutel', icon: '', label: 'Kotbeutel', color: '#84A98C' }, - { type: 'bank', icon: '', label: 'Sitzbank', color: '#92400E' }, - { type: 'drinking_water',icon: '', label: 'Wasserstelle',color: '#0EA5E9' }, - { type: 'dog_park', icon: '', label: 'Hundewiese', color: '#15803D' }, - { type: 'parkplatz', icon: '', label: 'Parkplatz', color: '#2563EB' }, - { type: 'treffpunkt', icon: '', label: 'Treffpunkt', color: '#7C3AED' }, - { type: 'sonstiges', icon: '', label: 'Sonstiges', color: '#F59E0B' }, + { type: 'giftkoeder', icon: '', label: 'Giftköder', color: '#DC2626' }, // ← wichtigster Typ, immer oben + { type: 'waste_basket', icon: '', label: 'Mülleimer', color: '#6B7280' }, + { type: 'kotbeutel', icon: '', label: 'Kotbeutel', color: '#84A98C' }, + { type: 'drinking_water', icon: '', label: 'Wasserstelle', color: '#0EA5E9' }, + { type: 'dog_park', icon: '', label: 'Hundewiese', color: '#15803D' }, + { type: 'parkplatz', icon: '', label: 'Parkplatz', color: '#2563EB' }, + { type: 'treffpunkt', icon: '', label: 'Treffpunkt', color: '#7C3AED' }, + { type: 'sonstiges', icon: '', label: 'Sonstiges', color: '#F59E0B' }, ]; function _confirmPlacement(latlng) { @@ -857,18 +855,18 @@ window.Page_map = (() => { radius: 10, color: '#F59E0B', fillColor: '#F59E0B', fillOpacity: 0.6, }).addTo(_map); - let _selectedTypes = new Set(['giftkoeder']); + let _selectedType = 'giftkoeder'; UI.modal.open({ title: ' Marker setzen', body: `
- +
${PIN_TYPES.map(p => ` @@ -894,21 +892,9 @@ window.Page_map = (() => { document.querySelector('.poi-type-grid')?.addEventListener('click', e => { const btn = e.target.closest('.poi-type-btn'); if (!btn) return; - const t = btn.dataset.type; - if (btn.dataset.excl) { - _selectedTypes = new Set([t]); - document.querySelectorAll('.poi-type-btn').forEach(b => b.classList.toggle('selected', b.dataset.type === t)); - } else { - if (_selectedTypes.has('giftkoeder')) { - _selectedTypes.delete('giftkoeder'); - document.querySelector('[data-excl="1"]')?.classList.remove('selected'); - } - if (_selectedTypes.has(t)) { - if (_selectedTypes.size > 1) { _selectedTypes.delete(t); btn.classList.remove('selected'); } - } else { - _selectedTypes.add(t); btn.classList.add('selected'); - } - } + document.querySelectorAll('.poi-type-btn').forEach(b => b.classList.remove('selected')); + btn.classList.add('selected'); + _selectedType = btn.dataset.type; }); document.getElementById('poi-cancel')?.addEventListener('click', () => { @@ -919,9 +905,8 @@ window.Page_map = (() => { document.getElementById('poi-save')?.addEventListener('click', async () => { const name = document.getElementById('poi-name').value.trim() || null; const notiz = document.getElementById('poi-notiz').value.trim() || null; - const type = [..._selectedTypes].join(','); UI.modal.close(); - await _saveUserPoi({ type, lat: latlng.lat, lon: latlng.lng, name, notiz }); + await _saveUserPoi({ type: _selectedType, lat: latlng.lat, lon: latlng.lng, name, notiz }); _exitPlacementMode(); }); } diff --git a/backend/static/js/pages/moderation.js b/backend/static/js/pages/moderation.js index 031fc11..5281850 100644 --- a/backend/static/js/pages/moderation.js +++ b/backend/static/js/pages/moderation.js @@ -88,35 +88,45 @@ window.Page_moderation = (() => { // ------------------------------------------------------------------ // TAB: ÜBERSICHT // ------------------------------------------------------------------ - function _switchTab(tabId) { - _tab = tabId; - _container.querySelectorAll('#mod-tabs .by-tab').forEach(b => - b.classList.toggle('active', b.dataset.tab === _tab) - ); - _renderTab(); - } - async function _renderStats(el) { const s = await API.get('/moderation/stats'); el.innerHTML = `
- ${_statCard('warning', 'Offene Meldungen', s.open_reports, s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)', 'forum')} - ${_statCard('image', 'Fotos ausstehend', s.pending_fotos, s.pending_fotos > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)', 'fotos')} - ${_statCard('skull', 'Gesperrte User', s.banned_users, s.banned_users > 0 ? '#f59e0b' : 'var(--c-text-muted)', 'user')} - ${_statCard('storefront','Züchter ausstehend',s.pending_zuchter, s.pending_zuchter > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)', 'user')} - ${_statCard('clock', 'POI-Korrekturen', s.pending_poi_edits ?? 0,(s.pending_poi_edits ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)', 'poi-edits')} + ${_statCard('warning', + 'Offene Meldungen', + s.open_reports, + s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)')} + ${_statCard('image', + 'Fotos ausstehend', + s.pending_fotos, + s.pending_fotos > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')} + ${_statCard('skull', + 'Gesperrte User', + s.banned_users, + s.banned_users > 0 ? '#f59e0b' : 'var(--c-text-muted)')} + ${_statCard('storefront', + 'Züchter ausstehend', + s.pending_zuchter, + s.pending_zuchter > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')} + ${_statCard('clock', + 'POI-Korrekturen', + s.pending_poi_edits ?? 0, + (s.pending_poi_edits ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}
+
+

+ ${UI.icon('info')} + Das Moderations-Panel zeigt dir alle ausstehenden Aufgaben auf einen Blick. + Verwende die Tabs oben für Details zu Fotos, Usern und Forum-Meldungen. +

+
`; - el.querySelectorAll('.mod-stat-card[data-tab]').forEach(card => { - card.addEventListener('click', () => _switchTab(card.dataset.tab)); - }); } - function _statCard(icon, label, value, color, tab) { - const clickable = tab ? `data-tab="${tab}" style="padding:var(--space-4);text-align:center;cursor:pointer;transition:box-shadow .15s,transform .15s" onmouseenter="this.style.boxShadow='var(--shadow-md)';this.style.transform='translateY(-2px)'" onmouseleave="this.style.boxShadow='';this.style.transform=''"` : `style="padding:var(--space-4);text-align:center"`; + function _statCard(icon, label, value, color) { return ` -
+
${label}
- ${tab ? `
${UI.icon('arrow-right')} öffnen
` : ''}
`; } @@ -208,16 +217,7 @@ window.Page_moderation = (() => { await API.patch(`/moderation/fotos/${btn.dataset.id}`, { action: 'approve' }); UI.toast('Foto freigegeben.', 'success'); await _loadFotos(el); - } catch (e) { - if (e.status === 404) { - UI.toast('Bereits bearbeitet — Liste aktualisiert.', 'info'); - await _loadFotos(el); - } else { - UI.toast(e.message, 'danger'); - btn.disabled = false; - btn.textContent = '✓ Freigeben'; - } - } + } catch (e) { UI.toast(e.message, 'danger'); btn.disabled = false; btn.textContent = '✓ Freigeben'; } }); }); diff --git a/backend/static/js/pages/personality.js b/backend/static/js/pages/personality.js deleted file mode 100644 index e1f159b..0000000 --- a/backend/static/js/pages/personality.js +++ /dev/null @@ -1,480 +0,0 @@ -/* ============================================================ - BAN YARO — Hunde-Persönlichkeitstest - 10 Fragen, 4 Typen: Abenteurer / Entdecker / Kuschler / Denker - ============================================================ */ - -window.Page_personality = (() => { - - let _container = null; - let _appState = null; - let _current = 0; // Aktuelle Frage (0-basiert) - let _scores = { A:0, B:0, C:0, D:0 }; - let _answers = []; // Gewählte Typen je Frage - - const LS_KEY = 'banyaro_personality_result'; - - // ---------------------------------------------------------- - // FRAGEN - // ---------------------------------------------------------- - const FRAGEN = [ - { frage: "Wie reagiert dein Hund auf neue Orte?", - antworten: [ - { text: "Stürmt sofort los — alles erkunden!", typ: 'A' }, - { text: "Schaut erst vorsichtig, dann neugierig", typ: 'B' }, - { text: "Bleibt lieber bei mir in der Nähe", typ: 'C' }, - { text: "Analysiert die Lage gründlich", typ: 'D' }, - ]}, - { frage: "Was macht dein Hund am liebsten?", - antworten: [ - { text: "Rennen, Ball, endlos spielen", typ: 'A' }, - { text: "Schnüffeln und die Welt erkunden", typ: 'B' }, - { text: "Kuscheln auf dem Sofa", typ: 'C' }, - { text: "Tricks lernen und Aufgaben lösen", typ: 'D' }, - ]}, - { frage: "Wie verhält er sich mit anderen Hunden?", - antworten: [ - { text: "Spielt sofort und wild mit", typ: 'A' }, - { text: "Friendly, aber wählerisch", typ: 'B' }, - { text: "Lieber zu zweit als in der Gruppe", typ: 'C' }, - { text: "Beobachtet erstmal genau", typ: 'D' }, - ]}, - { frage: "Wie reagiert er auf Kommandos?", - antworten: [ - { text: "Macht alles — wenn er Lust hat 😅", typ: 'A' }, - { text: "Gut, aber manchmal abgelenkt", typ: 'B' }, - { text: "Sehr zuverlässig, will gefallen", typ: 'C' }, - { text: "Präzise und fokussiert", typ: 'D' }, - ]}, - { frage: "Was passiert wenn du heimkommst?", - antworten: [ - { text: "Explosiver Freudentanz!", typ: 'A' }, - { text: "Wedelt freudig, bleibt aber cool", typ: 'B' }, - { text: "Kuschelt sich sofort an dich", typ: 'C' }, - { text: "Bringt dir sein Lieblingsspielzeug", typ: 'D' }, - ]}, - { frage: "Wie ist er bei Geräuschen/Gewitter?", - antworten: [ - { text: "Interessiert sich dafür oder ignoriert es", typ: 'A' }, - { text: "Schaut kurz, dann weiter", typ: 'B' }, - { text: "Sucht Schutz bei dir", typ: 'C' }, - { text: "Analysiert die Situation", typ: 'D' }, - ]}, - { frage: "Sein Verhältnis zu Kindern?", - antworten: [ - { text: "Liebt das wilde Spielen!", typ: 'A' }, - { text: "Gut, aber auf seine Art", typ: 'B' }, - { text: "Sanft und geduldig", typ: 'C' }, - { text: "Vorsichtig, aber freundlich", typ: 'D' }, - ]}, - { frage: "Was macht er alleine zu Hause?", - antworten: [ - { text: "Schläft oder spielt mit Spielzeug", typ: 'A' }, - { text: "Schaut aus dem Fenster", typ: 'B' }, - { text: "Wartet sehnsüchtig auf dich", typ: 'C' }, - { text: "Sucht sich Beschäftigung", typ: 'D' }, - ]}, - { frage: "Beim Gassigehen:", - antworten: [ - { text: "Zieht an der Leine — immer vorwärts!", typ: 'A' }, - { text: "Läuft locker aber entdeckungsfreudig", typ: 'B' }, - { text: "Bleibt gerne neben dir", typ: 'C' }, - { text: "Systematisches Schnüffeln", typ: 'D' }, - ]}, - { frage: "Was sagt er über dich aus?", - antworten: [ - { text: "Mein Mensch hält mit mir mit!", typ: 'A' }, - { text: "Gibt mir Freiheit und Abenteuer", typ: 'B' }, - { text: "Mein bester Freund", typ: 'C' }, - { text: "Versteht mich wirklich", typ: 'D' }, - ]}, - ]; - - // ---------------------------------------------------------- - // TYPEN - // ---------------------------------------------------------- - const TYPEN = { - A: { - key: 'A', - emoji: '🏔️', - name: 'Der Abenteurer', - desc: 'Immer vorwärts, immer mehr! Dein Hund lebt im Augenblick und liebt das Unbekannte.', - staerken: ['Energiegeladen', 'Mutig', 'Lebensfroh'], - aktivitaeten: [ - { label: 'Routen', page: 'routes' }, - { label: 'Karte', page: 'map' }, - { label: 'Training', page: 'uebungen' }, - ], - aktivitaetLabels: ['Agility', 'Canicross', 'Lange Wanderungen', 'Nasenarbeit'], - rassen: ['Husky', 'Malinois', 'Border Collie'], - color: '#f97316', - bg: 'linear-gradient(135deg, #f97316, #ea580c)', - }, - B: { - key: 'B', - emoji: '🌍', - name: 'Der Entdecker', - desc: 'Neugierig auf alles, aber mit Köpfchen. Dein Hund ist der perfekte Begleiter für jede Situation.', - staerken: ['Anpassungsfähig', 'Sozial', 'Ausgeglichen'], - aktivitaeten: [ - { label: 'Karte', page: 'map' }, - { label: 'Events', page: 'events' }, - { label: 'Routen', page: 'routes' }, - ], - aktivitaetLabels: ['Mantrailing', 'Dummy-Training', 'Gassi-Treffen'], - rassen: ['Labrador', 'Golden Retriever', 'Beagle'], - color: '#0ea5e9', - bg: 'linear-gradient(135deg, #0ea5e9, #0284c7)', - }, - C: { - key: 'C', - emoji: '🥰', - name: 'Der Kuschler', - desc: 'Verbundenheit über alles. Dein Hund liebt Menschen mehr als alles andere.', - staerken: ['Loyal', 'Einfühlsam', 'Zuverlässig'], - aktivitaeten: [ - { label: 'Tagebuch', page: 'diary' }, - { label: 'Training', page: 'uebungen' }, - { label: 'Gesundheit', page: 'health' }, - ], - aktivitaetLabels: ['Trick-Training', 'Therapy-Dog-Ausbildung', 'Ruhige Spaziergänge'], - rassen: ['Cavalier KCS', 'Bichon Frisé', 'Mops'], - color: '#ec4899', - bg: 'linear-gradient(135deg, #ec4899, #db2777)', - }, - D: { - key: 'D', - emoji: '🧠', - name: 'Der Denker', - desc: 'Stratege mit Seele. Dein Hund denkt bevor er handelt — und ist dabei brillant.', - staerken: ['Intelligent', 'Fokussiert', 'Lernbegeistert'], - aktivitaeten: [ - { label: 'Übungen', page: 'uebungen' }, - { label: 'Training', page: 'trainingsplaene' }, - { label: 'Wiki', page: 'wiki' }, - ], - aktivitaetLabels: ['Zieltraining', 'Geruchsarbeit', 'Rally Obedience', 'Intelligenzspielzeug'], - rassen: ['Poodle', 'Schäferhund', 'Rottweiler'], - color: '#8b5cf6', - bg: 'linear-gradient(135deg, #8b5cf6, #7c3aed)', - }, - }; - - // ---------------------------------------------------------- - // LIFECYCLE - // ---------------------------------------------------------- - function init(container, appState) { - _container = container; - _appState = appState; - _renderPage(); - } - - function refresh() {} - - function onDogChange() {} - - // ---------------------------------------------------------- - // RENDER EINSTIEG - // ---------------------------------------------------------- - function _renderPage() { - // Gespeichertes Ergebnis aus localStorage? - const saved = _loadResult(); - if (saved) { - _renderResult(TYPEN[saved.typ], saved.scores, true); - } else { - _renderIntro(); - } - } - - // ---------------------------------------------------------- - // INTRO - // ---------------------------------------------------------- - function _renderIntro() { - _container.innerHTML = ` -
-
-
🐾
-

- Hunde-Persönlichkeitstest -

-

- 10 Fragen — finde heraus welcher der 4 Persönlichkeitstypen deinen Hund am besten beschreibt! -

-
- ${Object.values(TYPEN).map(t => ` -
-
${t.emoji}
-
${t.name}
-
`).join('')} -
- -
-
`; - - document.getElementById('quiz-start-btn').addEventListener('click', _startQuiz); - } - - // ---------------------------------------------------------- - // QUIZ STARTEN - // ---------------------------------------------------------- - function _startQuiz() { - _current = 0; - _scores = { A:0, B:0, C:0, D:0 }; - _answers = []; - _renderQuestion(); - } - - // ---------------------------------------------------------- - // FRAGE RENDERN - // ---------------------------------------------------------- - function _renderQuestion() { - const q = FRAGEN[_current]; - const pct = Math.round((_current / FRAGEN.length) * 100); - - _container.innerHTML = ` -
- -
-
- - Frage ${_current + 1} von ${FRAGEN.length} - - ${pct}% -
-
-
-
-
- - -
-

${q.frage}

-
- - -
- ${q.antworten.map((a, i) => ` - `).join('')} -
-
`; - - _container.querySelectorAll('.quiz-answer-btn').forEach(btn => { - btn.addEventListener('click', () => _answerQuestion(btn.dataset.typ)); - btn.addEventListener('mouseenter', () => { - btn.style.borderColor = 'var(--c-primary)'; - btn.style.background = 'var(--c-primary-subtle, rgba(var(--c-primary-rgb,59,130,246),.08))'; - }); - btn.addEventListener('mouseleave', () => { - if (!btn.classList.contains('selected')) { - btn.style.borderColor = 'var(--c-border)'; - btn.style.background = 'var(--c-surface)'; - } - }); - }); - } - - // ---------------------------------------------------------- - // ANTWORT VERARBEITEN - // ---------------------------------------------------------- - function _answerQuestion(typ) { - _scores[typ]++; - _answers.push(typ); - _current++; - - if (_current < FRAGEN.length) { - // Kurze Animation — zeige Auswahl kurz grün - _renderQuestion(); - } else { - _calcAndShowResult(); - } - } - - // ---------------------------------------------------------- - // AUSWERTUNG - // ---------------------------------------------------------- - function _calcAndShowResult() { - // Mehrheits-Typ finden; bei Gleichstand letzter bestimmender Typ - let maxScore = 0; - let winner = _answers[_answers.length - 1]; // Fallback: letzte Antwort - for (const [typ, score] of Object.entries(_scores)) { - if (score > maxScore) { - maxScore = score; - winner = typ; - } - } - // Bei Gleichstand: letzter in _answers der einen der Max-Score-Typen hat - const maxTypes = Object.entries(_scores) - .filter(([, s]) => s === maxScore) - .map(([t]) => t); - if (maxTypes.length > 1) { - for (let i = _answers.length - 1; i >= 0; i--) { - if (maxTypes.includes(_answers[i])) { winner = _answers[i]; break; } - } - } - - _saveResult(winner, _scores); - _renderResult(TYPEN[winner], _scores, false); - } - - // ---------------------------------------------------------- - // ERGEBNIS RENDERN - // ---------------------------------------------------------- - function _renderResult(typ, scores, fromStorage) { - const dogName = _appState?.activeDog?.name || 'dein Hund'; - const shareText = `${dogName} ist ${typ.name} ${typ.emoji} — macht den Test auf ban.yaro.de!`; - - const scoreBars = Object.entries(scores) - .sort(([,a],[,b]) => b - a) - .map(([t, s]) => { - const tp = TYPEN[t]; - const pct = Math.round((s / FRAGEN.length) * 100); - return ` -
- ${tp.emoji} -
-
-
-
-
- ${s}/${FRAGEN.length} -
`; - }).join(''); - - _container.innerHTML = ` -
- -
-
${typ.emoji}
-
Persönlichkeitstyp
-

${typ.name}

-

${typ.desc}

-
- - -
-
Stärken
-
- ${typ.staerken.map(s => ` - ${s}`).join('')} -
-
- - -
-
Empfohlene Aktivitäten
-
-
- ${typ.aktivitaetLabels.map(a => ` - ${a}`).join('')} -
-
- ${typ.aktivitaeten.map(a => ` - `).join('')} -
-
-
- - -
-
Typische Rassen
-
- ${typ.rassen.map(r => ` - ${r}`).join('')} -
-
- - -
-
Dein Profil
-
${scoreBars}
-
- - -
- - -
-
`; - - // Share - document.getElementById('quiz-share-btn')?.addEventListener('click', async () => { - if (navigator.share) { - try { - await navigator.share({ text: shareText, url: 'https://ban.yaro.de' }); - } catch {} - } else { - await navigator.clipboard.writeText(shareText); - UI.toast.success('In die Zwischenablage kopiert!'); - } - }); - - // Neustart - document.getElementById('quiz-restart-btn')?.addEventListener('click', () => { - localStorage.removeItem(LS_KEY); - _startQuiz(); - }); - } - - // ---------------------------------------------------------- - // LOCALSTORAGE - // ---------------------------------------------------------- - function _saveResult(typ, scores) { - try { - localStorage.setItem(LS_KEY, JSON.stringify({ typ, scores, ts: Date.now() })); - } catch {} - } - - function _loadResult() { - try { - const raw = localStorage.getItem(LS_KEY); - if (!raw) return null; - return JSON.parse(raw); - } catch { return null; } - } - - // ---------------------------------------------------------- - // PUBLIC - // ---------------------------------------------------------- - return { init, refresh, onDogChange }; - -})(); diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index deb71ae..bdc48c1 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -695,60 +695,10 @@ window.Page_routes = (() => { _recActive = true; _recTrack = []; _recDistKm = 0; _recStartTime = Date.now(); - // iOS-Hinweis: Display muss wach bleiben - if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { - const banner = document.createElement('div'); - banner.style.cssText = 'position:absolute;top:0;left:0;right:0;z-index:960;' + - 'background:rgba(30,30,30,0.92);color:#fff;font-size:13px;line-height:1.4;' + - 'padding:10px 14px;display:flex;align-items:flex-start;gap:10px;' + - 'border-bottom:1px solid rgba(255,255,255,0.1)'; - banner.innerHTML = ` - - Display wach lassen! Auf iPhone stoppt die GPS-Aufzeichnung, wenn das Display ausgeht — Helligkeit hochsetzen oder Bildschirm nicht sperren.`; - document.getElementById('rk-rec-map-wrap')?.appendChild(banner); - setTimeout(() => banner.remove(), 9000); - } - const ctrl = document.getElementById('rk-rec-ctrl'); - ctrl.innerHTML = ` - `; - - // Long-Press 1.8s zum Stoppen - let _stopTimer = null, _stopTick = null; - const btn = ctrl.querySelector('#rk-rec-stopbtn'); - const fill = ctrl.querySelector('#rk-stop-fill'); - const startHold = () => { - if (_stopTimer) return; - const DURATION = 1800; - const start = Date.now(); - _stopTick = setInterval(() => { - const p = Math.min((Date.now() - start) / DURATION, 1); - fill.style.transition = 'none'; - fill.style.transform = `scaleX(${p})`; - }, 30); - _stopTimer = setTimeout(() => { - clearInterval(_stopTick); _stopTick = null; _stopTimer = null; - fill.style.transform = 'scaleX(1)'; - _stopRecInOvl(true); - }, DURATION); - }; - const cancelHold = () => { - if (!_stopTimer && !_stopTick) return; - clearTimeout(_stopTimer); clearInterval(_stopTick); - _stopTimer = null; _stopTick = null; - fill.style.transition = 'transform 0.25s ease'; - fill.style.transform = 'scaleX(0)'; - }; - btn.addEventListener('pointerdown', e => { e.preventDefault(); startHold(); }); - btn.addEventListener('pointerup', cancelHold); - btn.addEventListener('pointerleave', cancelHold); - btn.addEventListener('pointercancel', cancelHold); + ctrl.innerHTML = ``; + ctrl.querySelector('#rk-rec-stopbtn').addEventListener('click', () => _stopRecInOvl(true)); document.getElementById('rk-rec-stats-bar').style.display = ''; _recPolyline = L.polyline([], { color: '#ef4444', weight: 5, opacity: 0.9 }).addTo(_recMap); @@ -839,7 +789,7 @@ window.Page_routes = (() => { dim.style.display = 'flex'; _recDimmed = true; } - }, 5000); + }, 10000); } async function _stopRecInOvl(save) { diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index b829dea..8829bb6 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -229,7 +229,6 @@ window.Page_settings = (() => {
-
@@ -314,7 +313,7 @@ window.Page_settings = (() => { -
+
KI-Notiz-Assistent
@@ -337,30 +336,6 @@ window.Page_settings = (() => {
- -
- -
-
Goldene Gassi-Stunde täglich
-
- Täglich um 07:00 Uhr: bestes Wetterfenster für den Gassi-Gang -
-
- -
-
@@ -443,88 +418,10 @@ window.Page_settings = (() => { : `🔥 Noch kein Streak — heute aktiv werden!`; } - // Lifetime-km Balken mit Meilenstein-Markierungen - const lifetimeEl = document.getElementById('settings-lifetime-km'); - if (lifetimeEl) { - const km = s.total_km ?? 0; - const MILESTONES = [ - { km: 100, label: '100', badge: '100-km-Club', color: '#cd7f32' }, - { km: 500, label: '500', badge: '500-km-Wanderer', color: '#94a3b8' }, - { km: 1000, label: '1k', badge: 'Tausend-km-Held', color: '#f59e0b' }, - { km: 5000, label: '5k', badge: 'Ultraläufer', color: '#cbd5e1' }, - ]; - const maxKm = 5000; - const pct = Math.min(km / maxKm * 100, 100); - const nextM = MILESTONES.find(m => km < m.km); - const reachedM = MILESTONES.filter(m => km >= m.km); - const lastBadge = reachedM.length ? reachedM[reachedM.length - 1] : null; - - const markers = MILESTONES.map(m => { - const pos = (m.km / maxKm * 100).toFixed(1); - const reached = km >= m.km; - return `
-
-
${m.label}
`; - }).join(''); - - lifetimeEl.innerHTML = ` -
- 🐾 Lebenswerk-km - ${km} km -
-
-
-
- ${markers} -
- ${nextM - ? `
- Noch ${(nextM.km - km).toLocaleString('de-DE')} km - bis ${nextM.badge} -
` - : `
- Ultraläufer-Legende erreicht! 🏆 -
`} -
`; - } - if (badgesEl && a.categories) { - // Foto-Hintergründe für bestimmte Badge-Kategorien - const _BADGE_PHOTOS = { - 'schnee_held': '/img/banyaro/winter_schnee.webp', - 'jahreszeiten': '/img/banyaro/herbst_bach.webp', - 'wetter_tapfer': '/img/banyaro/fruehling_playdate.webp', - }; - - // SVG-Schild für jede Kategorie (mit optionalem Foto-Hintergrund) - const shield = (color, dark, emoji, opacity = 1, catId = '') => { - const photo = _BADGE_PHOTOS[catId]; - const clipId = `clip_${catId || Math.random().toString(36).slice(2)}`; - const path = 'M30 3 L57 15 L57 38 Q57 60 30 70 Q3 60 3 38 L3 15 Z'; - if (photo && opacity === 1) { - return ` - - - - - - - ${emoji} - `; - } - return ` ` + @@ -532,12 +429,13 @@ window.Page_settings = (() => { - - + + ${emoji} `; - }; badgesEl.innerHTML = (a.categories || []).map(cat => { const cur = cat.current_tier; @@ -552,8 +450,8 @@ window.Page_settings = (() => { // Aktuelles Schild const shieldSvg = cur - ? shield(cur.color, cur.dark, cat.emoji, 1, cat.id) - : shield('#9ca3af', '#6b7280', cat.emoji, 0.5, cat.id); + ? shield(cur.color, cur.dark, cat.emoji) + : shield('#9ca3af', '#6b7280', cat.emoji, 0.5); // Fortschrittsbalken const progressBar = nxt ? ` @@ -887,25 +785,6 @@ window.Page_settings = (() => { } }); - document.getElementById('toggle-gassi-stunde')?.addEventListener('change', async e => { - const enabled = e.target.checked; - const track = document.getElementById('toggle-gassi-stunde-track'); - const thumb = document.getElementById('toggle-gassi-stunde-thumb'); - if (track) track.style.background = enabled ? 'var(--c-primary)' : 'var(--c-border)'; - if (thumb) thumb.style.left = enabled ? '22px' : '2px'; - try { - await API.patch('/profile', { gassi_stunde_push: enabled ? 1 : 0 }); - _appState.user.gassi_stunde_push = enabled ? 1 : 0; - UI.toast.success(enabled ? 'Goldene Gassi-Stunde aktiviert.' : 'Goldene Gassi-Stunde deaktiviert.'); - } catch (err) { - UI.toast.error(err?.message || 'Einstellung konnte nicht gespeichert werden.'); - // Revert UI - e.target.checked = !enabled; - if (track) track.style.background = !enabled ? 'var(--c-primary)' : 'var(--c-border)'; - if (thumb) thumb.style.left = !enabled ? '22px' : '2px'; - } - }); - _loadReferral(); _loadBreederCard(); } @@ -1672,9 +1551,9 @@ window.Page_settings = (() => { _offerPushNotifications(); } - // Nach Login: Direkt in HUND-Welt oder Profil anlegen + // Nach Login: Welcome-Seite oder Profil anlegen if (_appState.activeDog) { - window.Worlds?.show(1); + App.navigate('welcome'); } else { App.navigate('dog-profile'); } diff --git a/backend/static/js/pages/uebungen.js b/backend/static/js/pages/uebungen.js index 38f9f08..fed9bac 100644 --- a/backend/static/js/pages/uebungen.js +++ b/backend/static/js/pages/uebungen.js @@ -34,7 +34,6 @@ window.Page_uebungen = (() => { // ---------------------------------------------------------- // STATS STATE // ---------------------------------------------------------- - let _helpHandle = null; // Rückgabe von UI.pageInfo — für inline Trigger-Button let _statsData = null; // cached stats from /api/training/stats let _badgesData = null; // cached badges from /api/achievements let _exercisesByTab = {}; // aus API geladen @@ -477,18 +476,6 @@ window.Page_uebungen = (() => { if (_VALID_TABS.has(mapped)) _activeTab = mapped; } _render(); - _helpHandle = UI.pageInfo(_container, { - pageId: 'uebungen', - title: 'Übungsbibliothek', - icon: 'graduation-cap', - intro: 'Hier findest du alle Übungen für deinen Hund — von Grundkommandos bis zu Tricks und Problemverhalten. Du kannst deinen Trainingsfortschritt für jede Übung festhalten.', - steps: [ - { icon: 'list-checks', title: 'Stand erfassen', text: 'Klicke auf "Stand erfassen" um schnell für alle Übungen einzutragen, was euer aktueller Stand ist.' }, - { icon: 'flag', title: 'Übung üben', text: 'Tippe auf eine Übung, um die Anleitung zu lesen. Mit den Fortschritts-Icons (Flagge → Trophäe) trackst du, wie weit ihr seid.' }, - { icon: 'star', title: 'KI-Trainer', text: 'Im Tab "KI-Trainer" analysiert unsere KI deinen Trainingsstand und gibt personalisierte Empfehlungen.' }, - ], - tip: 'Regelmäßiges Training stärkt die Bindung — auch 5 Minuten täglich machen einen großen Unterschied!', - }); // Übungen aus DB laden (parallel mit Progress) if (!_exercisesLoaded) { @@ -748,14 +735,10 @@ window.Page_uebungen = (() => { Dein Plan für heute -
${cards.join('')}
`; - if (_helpHandle) { - document.getElementById('ueb-help-anchor')?.appendChild(_helpHandle.makeTriggerBtn()); - } el.querySelectorAll('.ueb-trainer-btn').forEach(btn => { btn.addEventListener('click', () => { diff --git a/backend/static/js/pages/wetter.js b/backend/static/js/pages/wetter.js index 755b825..c838916 100644 --- a/backend/static/js/pages/wetter.js +++ b/backend/static/js/pages/wetter.js @@ -55,12 +55,11 @@ window.Page_wetter = (() => { // ---------------------------------------------------------- // MODUL-STATE // ---------------------------------------------------------- - let _container = null; - let _appState = null; - let _data = null; - let _selDay = 0; - let _loading = false; - let _recordsLoaded = false; + let _container = null; + let _appState = null; + let _data = null; + let _selDay = 0; + let _loading = false; // ---------------------------------------------------------- // INIT @@ -77,8 +76,7 @@ window.Page_wetter = (() => { // REFRESH // ---------------------------------------------------------- async function refresh() { - _selDay = 0; - _recordsLoaded = false; + _selDay = 0; _renderShell(); _tryAutoLocate(); } @@ -189,18 +187,9 @@ window.Page_wetter = (() => { style="margin-bottom:var(--space-4)"> - -
-
-
- - -
-
`; // Strip-Klick-Events @@ -209,15 +198,12 @@ window.Page_wetter = (() => { _selDay = parseInt(card.dataset.wttrDay); _updateStrip(); _renderDetail(); - _renderRainTimeline(); _renderDog(); }); }); _renderDetail(); - _renderRainTimeline(); _renderDog(); - _loadRecords(); } // ---------------------------------------------------------- @@ -332,9 +318,6 @@ window.Page_wetter = (() => { - - ${_gassiScoreBadge(d)} - ${sunriseStr && sunsetStr ? `
@@ -397,137 +380,6 @@ window.Page_wetter = (() => { `; } - // ---------------------------------------------------------- - // NIEDERSCHLAGS-ZEITSKALA (stündlich) - // ---------------------------------------------------------- - function _renderRainTimeline() { - const el = _container.querySelector('#wttr-rain'); - if (!el || !_data) return; - const d = (_data.days || [])[_selDay]; - if (!d) return; - - const hourly = d.hourly || []; - // Filtere auf Stunden mit Daten, die eine Niederschlagswahrscheinlichkeit haben - const entries = hourly.filter(h => h.precip_prob != null); - if (!entries.length) { el.style.display = 'none'; return; } - el.style.display = ''; - - // Für "Heute" (Tag 0): ab jetzt, sonst alle 24h - const now = new Date(); - const nowMin = now.getHours() * 60 + now.getMinutes(); - let slots = entries; - if (_selDay === 0) { - // Zeige ab der aktuellen Stunde (und die letzten 2h als Kontext) - const pastCutoff = now.getHours() - 2; - slots = entries.filter(h => { - const hHour = parseInt(h.hour.split(':')[0]); - return hHour >= pastCutoff; - }); - // Falls nichts übrig bleibt, zeige alles - if (!slots.length) slots = entries; - } - - // Max probability für Skalierung (mindestens 30 damit die Balken sichtbar sind) - const maxProb = Math.max(30, ...slots.map(h => h.precip_prob ?? 0)); - - // Farb-Funktion: blau basierend auf Wahrscheinlichkeit - function _rainColor(prob) { - if (prob < 10) return 'rgba(148,163,184,0.4)'; // grau, kaum Regen - if (prob < 25) return 'rgba(147,197,253,0.65)'; // hellblau - if (prob < 50) return 'rgba(96,165,250,0.8)'; // blau - if (prob < 75) return 'rgba(59,130,246,0.9)'; // kräftig blau - return 'rgba(29,78,216,1)'; // dunkelblau - } - - // Aktuell aktiver Slot (nur bei Heute) - const currentHour = now.getHours(); - - const bars = slots.map(h => { - const prob = h.precip_prob ?? 0; - const hHour = parseInt(h.hour.split(':')[0]); - const isNow = _selDay === 0 && hHour === currentHour; - const barH = Math.max(2, Math.round((prob / 100) * 56)); // max 56px Balkenhöhe - const color = _rainColor(prob); - const labelHour = h.hour.substring(0, 2); // 'HH' - - return ` -
- -
- ${prob >= 20 ? prob + '%' : ''} -
- -
-
-
- -
- ${isNow ? 'jetzt' : labelHour + 'h'} -
-
- `; - }).join(''); - - // Gibt es überhaupt nennenswerten Niederschlag? - const hasRain = slots.some(h => (h.precip_prob ?? 0) >= 10); - const titleColor = hasRain ? '#60A5FA' : 'var(--c-text-secondary)'; - const titleIcon = hasRain ? 'cloud-rain' : 'cloud'; - - el.innerHTML = ` -
- - - - - Niederschlagswahrscheinlichkeit - - - ${_selDay === 0 ? 'heute' : _esc(d.date ? new Date(d.date + 'T12:00').toLocaleDateString('de', {weekday:'short', day:'numeric', month:'short'}) : '')} - -
- -
-
-
- ${bars} -
-
- -
-
- ${!hasRain ? ` -
- Kein Regen erwartet -
` : ''} - `; - - // Scroll zum aktuellen Slot wenn Heute - if (_selDay === 0) { - requestAnimationFrame(() => { - const wrap = el.querySelector('div[style*="overflow-x"]'); - if (!wrap) return; - const nowIdx = slots.findIndex(h => parseInt(h.hour.split(':')[0]) === currentHour); - if (nowIdx > 2) { - wrap.scrollLeft = (nowIdx - 2) * 41; // ca. 38px + 3px gap - } - }); - } - } - // ---------------------------------------------------------- // HUNDE-WETTER // ---------------------------------------------------------- @@ -538,24 +390,11 @@ window.Page_wetter = (() => { if (!d) return; const _POLLEN_NAMES = { erle:'Erle', birke:'Birke', graeser:'Gräser', beifuss:'Beifuß', ambrosia:'Ambrosia' }; - const felltyp = (_appState?.activeDog ?? _appState?.dogs?.[0])?.fell_typ || null; - const _wl = _dogWeatherLabel(d, felltyp); - let html = ` -
-
${_wl.emoji}
-
- ${_esc(_wl.label)} -
-
- ${_esc(_wl.sub)} -
-
-

- - Hunde-Hinweise -

`; + let html = `

+ + Hunde-Wetter +

`; // Asphalt-Temperatur if (d.asphalt_temp != null) { @@ -661,45 +500,6 @@ window.Page_wetter = (() => { `; } - // Fell-spezifische Hinweise - if (felltyp) { - const tempNow = d.temp_max ?? 20; - let fellHint = null; - if (felltyp === 'doppel' && tempNow > 20) { - fellHint = { icon: 'thermometer-hot', color: '#F97316', - text: 'Doppeltes Fell — heute besonders auf Überhitzung achten.' }; - } else if (felltyp === 'nackt' && tempNow < 15) { - fellHint = { icon: 'coat-hanger', color: '#60A5FA', - text: 'Nackthund braucht heute eine Hundejacke oder einen -pullover.' }; - } else if (felltyp === 'kurz' && tempNow < 5) { - fellHint = { icon: 'snowflake', color: '#38BDF8', - text: 'Kurzhaar friert schnell — Hundemantel empfohlen.' }; - } - if (fellHint) { - html += ` -
- - - -
${_esc(fellHint.text)}
-
- `; - } - } - - // Schnüffel-Index + Hunde-Alter Chips - const ageYears = _dogAgeYears(); - html += _dogAgeChip(ageYears); - - html += ` -
- ${_schnueffelChip(d)} -
- `; - // Wenn keine Hunde-Daten vorhanden if (!d.asphalt_temp && !d.paw_cold && !d.thunderstorm && !d.zecken && !(pollen && Object.keys(pollen).length)) { @@ -713,160 +513,6 @@ window.Page_wetter = (() => { el.innerHTML = html; } - // ---------------------------------------------------------- - // GASSI-SCORE (1–10) - // ---------------------------------------------------------- - function _gassiScore(d) { - let score = 10; - const temp = d.temp_max ?? 20; - const precip = d.precip_prob ?? 0; - const wind = d.windspeed_max ?? 0; - const asphalt = d.asphalt_temp ?? 0; - - // Temperatur (ideal: 10–20°C) - if (temp > 30) score -= 3; - else if (temp > 25) score -= 1; - else if (temp < 0) score -= 3; - else if (temp < 5) score -= 1; - - // Regen - if (precip > 70) score -= 3; - else if (precip > 40) score -= 2; - else if (precip > 20) score -= 1; - - // Wind - if (wind > 60) score -= 2; - else if (wind > 40) score -= 1; - - // Asphalt - if (asphalt > 55) score -= 2; - else if (asphalt > 45) score -= 1; - - // Gewitter - if (d.thunderstorm) score -= 3; - - return Math.max(1, Math.min(10, score)); - } - - function _gassiScoreBadge(d) { - const score = _gassiScore(d); - let color, text; - if (score >= 8) { - color = '#10B981'; - text = 'Toller Gassi-Tag!'; - } else if (score >= 5) { - color = '#F59E0B'; - text = 'Geht so'; - } else { - color = '#EF4444'; - text = 'Lieber drinbleiben'; - } - return ` -
- 🐾 Gassi-Score - - ${score} - - / 10 - — ${_esc(text)} -
- `; - } - - // ---------------------------------------------------------- - // SCHNÜFFEL-INDEX - // ---------------------------------------------------------- - function _schnueffelIndex(d) { - const temp = d.temp_max ?? 20; - const precip = d.precip_prob ?? 0; - - // Feuchtigkeit aus precip_prob ableiten - const feucht = precip > 60 ? 'feucht' : precip > 30 ? 'leicht-feucht' : 'trocken'; - - if (feucht === 'feucht' && temp >= 10 && temp <= 18) - return { label:'Exzellent 👃', color:'#10B981' }; - if (feucht === 'feucht' && temp > 10 && temp <= 22) - return { label:'Sehr gut 👃', color:'#34D399' }; - if (temp < 5) - return { label:'Gut (kalte Luft trägt Gerüche)', color:'#60A5FA' }; - if (feucht === 'leicht-feucht' && temp >= 10 && temp <= 22) - return { label:'Gut 👃', color:'#4CAF50' }; - if (feucht === 'trocken' && temp > 25) - return { label:'Schwach', color:'#94A3B8' }; - return { label:'Mittel', color:'#F59E0B' }; - } - - function _schnueffelChip(d) { - const s = _schnueffelIndex(d); - return ` - - - Schnüffel: ${_esc(s.label)} - - `; - } - - // ---------------------------------------------------------- - // HUNDE-ALTER aus appState - // ---------------------------------------------------------- - function _dogAgeYears() { - try { - const dog = _appState?.activeDog || _appState?.dog || _appState?.active_dog; - if (!dog) return null; - const geb = dog.geburtsdatum || dog.birthdate; - if (!geb) return null; - const birth = new Date(geb); - if (isNaN(birth)) return null; - const now = new Date(); - let age = now.getFullYear() - birth.getFullYear(); - const m = now.getMonth() - birth.getMonth(); - if (m < 0 || (m === 0 && now.getDate() < birth.getDate())) age--; - return age < 0 ? 0 : age; - } catch { return null; } - } - - function _dogAgeChip(ageYears) { - if (ageYears === null) return ''; - if (ageYears < 1) { - return ` -
- -
- Welpe — kurze Spaziergänge, max. 15 Min bei Hitze. - Gelenke und Pfoten besonders schonen. -
-
- `; - } - if (ageYears >= 8) { - return ` -
- -
- Seniorhund — Hitze und Kälte vermeiden, kurze Runden bevorzugen. - Auf Gelenkbeschwerden achten. -
-
- `; - } - return ''; - } - // ---------------------------------------------------------- // HILFSFUNKTIONEN — Wetter // ---------------------------------------------------------- @@ -911,58 +557,6 @@ window.Page_wetter = (() => { return '#F44336'; // level 4+ } - function _dogWeatherLabel(d, felltyp) { - const temp = d.temp_max ?? 20; - const tempMin = d.temp_min ?? temp; - const precip = d.precip_prob ?? 0; - const wind = d.windspeed_max ?? 0; - const asphalt = d.asphalt_temp ?? 0; - const wcode = d.weathercode ?? 0; - const isSnow = wcode >= 71 && wcode <= 77; - - // Fell-spezifische Temperaturschwellen - const heatLimit = { - kurz: 25, mittel: 27, lang: 22, drahtaar: 26, doppel: 30, nackt: 20 - }[felltyp] ?? 28; - const coldLimit = { - kurz: 8, mittel: 5, lang: 3, drahtaar: 5, doppel: -5, nackt: 15 - }[felltyp] ?? 5; - - if (d.thunderstorm) - return { label:'Gewitterangst-Wetter', sub:'Angsthasen lieber zu Hause lassen', emoji:'⛈️', color:'#7C3AED' }; - if (isSnow && temp < 3) - return { label:'Schnee-Toben-Wetter', sub:'Pudel im Schnee — der Klassiker', emoji:'❄️', color:'#38BDF8' }; - if (isSnow) - return { label:'Matschpfoten-Wetter', sub:'Pfoten nach der Runde gut abtrocknen', emoji:'🌨️', color:'#60A5FA' }; - if (tempMin < 0 && precip < 30) - return { label:'Kristallklare Nasenluft', sub:'Kalt aber herrlich — Schnüffeln auf Maximum', emoji:'🌡️', color:'#60A5FA' }; - if (temp < coldLimit && precip > 50) - return { label:'Kuschelwetter', sub:'Kurze Runde, dann ab auf das Sofa', emoji:'🏠', color:'#6B7280' }; - if (temp < coldLimit) - return { label:'Fellkuschelwetter', sub:'Frisch und klar — ideal für aktive Rassen', emoji:'🧣', color:'#93C5FD' }; - if (temp > heatLimit && asphalt > 50) - return { label:'Pfoten-Alarm!', sub:'Asphalt zu heiß — früh morgens oder abends raus', emoji:'🔥', color:'#EF4444' }; - if (temp > heatLimit) - return { label:'Schwimm-Wetter', sub:'Bach oder See suchen — Hunde überhitzen schnell', emoji:'🏊', color:'#F97316' }; - if (precip > 70 && temp < 15) - return { label:'Nass-Hund-Wetter', sub:'Handtuch bereit? Der Geruch kommt garantiert', emoji:'💧', color:'#3B82F6' }; - if (precip > 70) - return { label:'Warm-Dusch-Wetter', sub:'Wer braucht noch ein Bad — der Regen übernimmt', emoji:'🌧️', color:'#60A5FA' }; - if (precip > 30 && temp >= 10 && temp <= 20) - return { label:'Schnüffel-Wetter', sub:'Feuchte Luft = Nasenarbeit pur — Gerüche lieben das', emoji:'👃', color:'#34D399' }; - if (wind > 50) - return { label:'Sturmfrisur-Wetter', sub:'Fell in alle Richtungen — Leine gut festhalten', emoji:'🌬️', color:'#A78BFA' }; - if (wind > 30 && temp >= 15) - return { label:'Ohren-im-Wind-Wetter', sub:'Optimal für Hunde mit Schlappohren', emoji:'💨', color:'#A78BFA' }; - if (precip > 30 && precip <= 70) - return { label:'Gassiregen-Wetter', sub:'Leichte Jacke, kurze Runde — Hund findet es gut', emoji:'🌦️', color:'#60A5FA' }; - if (temp >= 18 && temp <= 26 && precip < 20) - return { label:'Perfektes Gassi-Wetter',sub:'Heute müssen alle Routen genossen werden', emoji:'🐾', color:'#10B981' }; - if (temp >= 10 && temp < 18 && precip < 30) - return { label:'Klassisches Hunde-Wetter', sub:'Nicht zu warm, nicht zu kalt — Vierbeiner-Paradies', emoji:'🐕', color:'#4CAF50' }; - return { label:'Gutes Hunde-Wetter', sub:'Raus mit dem Hund!', emoji:'🐶', color:'#10B981' }; - } - function _tickLevel(risk) { const r = (risk || '').toLowerCase(); if (r === 'niedrig') return ['niedrig', '#4CAF50']; @@ -979,104 +573,6 @@ window.Page_wetter = (() => { .replace(/"/g, '"'); } - // ---------------------------------------------------------- - // MEINE WETTERREKORDE - // ---------------------------------------------------------- - async function _loadRecords() { - // Nur wenn User eingeloggt - if (!_appState?.user) return; - // Nur einmal pro Seitenaufruf laden - if (_recordsLoaded) return; - _recordsLoaded = true; - try { - const res = await API.get('/weather/records'); - _renderRecords(res?.records || null); - } catch { - // Stumm scheitern — Rekorde sind ein Nice-to-have - } - } - - function _fmtDate(datum) { - if (!datum) return ''; - try { - return new Date(datum + 'T12:00').toLocaleDateString('de', { - day: 'numeric', month: 'short', year: 'numeric' - }); - } catch { return datum; } - } - - function _recordCard(emoji, title, value, subtitle, color) { - return ` -
-
- ${emoji} - ${_esc(title)} -
-
- ${_esc(value)} -
-
- ${_esc(subtitle)} -
-
- `; - } - - function _renderRecords(records) { - const el = _container.querySelector('#wttr-records'); - if (!el) return; - - // Mindestens 3 Einträge nötig - if (!records || (records.gesamt_eintraege || 0) < 3) { - el.innerHTML = ''; - return; - } - - const cards = []; - - if (records.kaeltester) { - const e = records.kaeltester; - const sub = e.titel ? `${e.titel} · ${_fmtDate(e.datum)}` : _fmtDate(e.datum); - cards.push(_recordCard('🥶', 'Kältester Gassi', `${Math.round(e.temp_c)}°C`, sub, '#60A5FA')); - } - - if (records.heissester) { - const e = records.heissester; - const sub = e.titel ? `${e.titel} · ${_fmtDate(e.datum)}` : _fmtDate(e.datum); - cards.push(_recordCard('🔥', 'Heißester Gassi', `${Math.round(e.temp_c)}°C`, sub, '#EF4444')); - } - - if (records.stuermischster) { - const e = records.stuermischster; - const sub = e.titel ? `${e.titel} · ${_fmtDate(e.datum)}` : _fmtDate(e.datum); - cards.push(_recordCard('🌬️', 'Stürmischster Tag', `${Math.round(e.wind_kmh)} km/h`, sub, '#A78BFA')); - } - - const regenCount = records.regen_eintraege || 0; - const gesamt = records.gesamt_eintraege || 0; - cards.push(_recordCard('💧', 'Regentage', `${regenCount} Einträge`, `von ${gesamt} Tagebucheinträgen`, '#3B82F6')); - - el.innerHTML = ` -
-

- - - - Meine Wetterrekorde -

-
- ${cards.join('')} -
-
- `; - } - // ---------------------------------------------------------- // PUBLIC API // ---------------------------------------------------------- diff --git a/backend/static/js/pages/wiki.js b/backend/static/js/pages/wiki.js index b0c101d..b2b6390 100644 --- a/backend/static/js/pages/wiki.js +++ b/backend/static/js/pages/wiki.js @@ -730,34 +730,36 @@ window.Page_wiki = (() => { : ''; const _dogSvgLg = _DOG_SILHOUETTE.replace('width="48" height="48"', 'width="56" height="56"'); - // Alle Fotos: Hauptbild zuerst, dann Community-Fotos - const allFotos = []; - if (rasse.foto_url) allFotos.push({ foto_url: rasse.foto_url, user_name: null }); - (rasse.user_fotos || []).forEach(f => allFotos.push(f)); - - const photoHtml = allFotos.length - ? `` + const photoHtml = rasse.foto_url + ? `
+ ${_esc(rasse.name)} +
+ ` : `
${_dogSvgLg}Kein Foto verfügbar
`; const berichteHtml = _renderBerichteHtml(rasse.berichte || [], slug); - const userFotosHtml = ''; + + const userFotosHtml = (rasse.user_fotos || []).length + ? `
+
Community-Fotos
+
+ ${rasse.user_fotos.map(f => ` +
+ ${_esc(f.user_name)} +
von ${_esc(f.user_name)}
+
+ `).join('')} +
+
` + : ''; const body = ` ${/* 1. Hero */ ''} @@ -849,65 +851,6 @@ window.Page_wiki = (() => { document.getElementById('wiki-zuchter-placeholder')?.remove(); }); - // Gallery-Thumbnails + Lightbox - const mainImg = document.getElementById('wiki-main-photo'); - const strip = document.getElementById('wiki-gallery-strip'); - if (strip && mainImg) { - strip.querySelectorAll('.wiki-gallery-thumb').forEach(btn => { - btn.addEventListener('click', () => { - const idx = parseInt(btn.dataset.idx); - mainImg.src = allFotos[idx].foto_url; - mainImg.style.display = ''; - document.getElementById('wiki-photo-fallback').style.display = 'none'; - strip.querySelectorAll('.wiki-gallery-thumb').forEach(b => b.classList.toggle('active', b === btn)); - }); - }); - } - - document.getElementById('wiki-gallery-expand')?.addEventListener('click', () => { - const src = mainImg?.src || allFotos[0]?.foto_url; - if (!src) return; - let curIdx = allFotos.findIndex(f => f.foto_url && src.endsWith(f.foto_url.split('/').pop())); - if (curIdx < 0) curIdx = 0; - - function _lbOpen(idx) { - const f = allFotos[idx]; - const lb = document.getElementById('wiki-lightbox'); - lb.querySelector('.wlb-img').src = f.foto_url; - lb.querySelector('.wlb-caption').textContent = f.user_name ? `Foto von ${f.user_name}` : rasse.name; - lb.querySelector('.wlb-counter').textContent = `${idx + 1} / ${allFotos.length}`; - lb.querySelector('.wlb-prev').style.display = allFotos.length > 1 ? '' : 'none'; - lb.querySelector('.wlb-next').style.display = allFotos.length > 1 ? '' : 'none'; - curIdx = idx; - } - - const lb = document.createElement('div'); - lb.id = 'wiki-lightbox'; - lb.innerHTML = ` -
-
- - - - -
-
-
`; - document.body.appendChild(lb); - _lbOpen(curIdx); - - const close = () => lb.remove(); - lb.querySelector('.wlb-close').addEventListener('click', close); - lb.querySelector('.wlb-backdrop').addEventListener('click', close); - lb.querySelector('.wlb-prev').addEventListener('click', () => _lbOpen((curIdx - 1 + allFotos.length) % allFotos.length)); - lb.querySelector('.wlb-next').addEventListener('click', () => _lbOpen((curIdx + 1) % allFotos.length)); - document.addEventListener('keydown', function onKey(e) { - if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); } - if (e.key === 'ArrowLeft') lb.querySelector('.wlb-prev').click(); - if (e.key === 'ArrowRight') lb.querySelector('.wlb-next').click(); - }); - }); - document.getElementById('wiki-bericht-add-btn')?.addEventListener('click', () => { UI.modal.close(); setTimeout(() => _showBerichtForm(slug, rasse.name), 350); diff --git a/backend/static/js/ui.js b/backend/static/js/ui.js index a4a550b..867f1c9 100644 --- a/backend/static/js/ui.js +++ b/backend/static/js/ui.js @@ -280,75 +280,6 @@ const UI = (() => { // Alias für ältere Aufrufe const escHtml = escape; - // ---------------------------------------------------------- - // PAGE INFO — generische Seiten-Hilfe - // config: { pageId, title, icon?, intro, steps?: [{icon,title,text}], tip? } - // Erstes Öffnen: expandierter Banner. Danach: kleines ? im Header. - // ---------------------------------------------------------- - function pageInfo(container, config) { - const seenKey = 'help_seen_' + config.pageId; - const seen = !!localStorage.getItem(seenKey); - - function _buildSteps() { - if (!config.steps?.length) return ''; - return config.steps.map(s => ` -
- ${s.icon ? `${_svgIcon(s.icon)}` : ''} -
- ${s.title ? `
${s.title}
` : ''} -
${s.text}
-
-
`).join(''); - } - - function _openModal() { - modal.open({ - title: `${_svgIcon(config.icon || 'question')} ${config.title}`, - body: ` -
-

${config.intro}

- ${config.steps?.length ? `
${_buildSteps()}
` : ''} - ${config.tip ? `
${_svgIcon('lightbulb')} ${config.tip}
` : ''} -
`, - }); - } - - // Kein automatischer absolut-positionierter Trigger mehr. - // Aufrufer kann openModal() nutzen und den Button selbst platzieren. - - // Banner beim ersten Besuch - if (!seen) { - localStorage.setItem(seenKey, '1'); - const banner = document.createElement('div'); - banner.className = 'pinfo-banner'; - banner.innerHTML = ` -
- ${_svgIcon(config.icon || 'info')} - ${config.title} - -
-
${config.intro}
- ${config.steps?.length ? `
${_buildSteps()}
` : ''} - - `; - banner.querySelector('.pinfo-banner-close').addEventListener('click', () => banner.remove()); - banner.querySelector('.pinfo-banner-more').addEventListener('click', () => { banner.remove(); _openModal(); }); - container.insertAdjacentElement('afterbegin', banner); - } - - // Inline-Trigger-Button (für Aufrufer zum Einbetten) - function makeTriggerBtn() { - const btn = document.createElement('button'); - btn.className = 'pinfo-trigger-inline'; - btn.setAttribute('aria-label', 'Hilfe'); - btn.innerHTML = _svgIcon('question'); - btn.addEventListener('click', _openModal); - return btn; - } - - return { openModal: _openModal, makeTriggerBtn }; - } - // ---------------------------------------------------------- // HELP TOOLTIP — inline ? Badge mit Klick-Tooltip // ---------------------------------------------------------- @@ -984,7 +915,7 @@ const UI = (() => { emptyState, time, setupPhotoPreview, scrollTop, skeleton, icon: _svgIcon, - escape, escHtml, help, pageInfo, + escape, escHtml, help, saveToAlbum, loadLeaflet, leafletMarker, diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js index daacde0..0fc7723 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -49,6 +49,9 @@ window.Worlds = (() => { _setupButtons(); _goTo(_cur, false); show(); + // Welten parallel rendern + _renderJetzt(); + _renderHund(); } function show(worldIdx) { @@ -63,17 +66,12 @@ window.Worlds = (() => { if (worldIdx != null) _goTo(worldIdx, false); if (_cur === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } - // Nach Login/Logout: Config aus DB laden, dann rendern + // Nach Login/Logout neu rendern const currentUserId = _state?.user?.id ?? null; if (currentUserId !== _lastUserId) { _lastUserId = currentUserId; - if (currentUserId) { - _loadConfigFromServer().then(() => { _renderJetzt(); _renderHund(); }); - } else { - _cfgCache = null; - _renderJetzt(); - _renderHund(); - } + _renderJetzt(); + _renderHund(); } } @@ -127,21 +125,6 @@ window.Worlds = (() => { _goTo(next, true); if (next === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } }); - - // Mausrad-Navigation (Desktop) - let _wheelCooldown = false; - track.addEventListener('wheel', e => { - e.preventDefault(); - if (_wheelCooldown) return; - const next = e.deltaX > 30 || e.deltaY > 30 ? Math.min(2, _cur + 1) - : e.deltaX < -30 || e.deltaY < -30 ? Math.max(0, _cur - 1) - : _cur; - if (next === _cur) return; - _wheelCooldown = true; - setTimeout(() => { _wheelCooldown = false; }, 500); - _goTo(next, true); - if (next === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } - }, { passive: false }); } function _goTo(idx, animated) { @@ -165,28 +148,18 @@ window.Worlds = (() => { document.querySelectorAll('.wlabel').forEach((l, i) => l.classList.toggle('active', i === _cur)); } - function _fabOptions() { - const worldNames = ['jetzt', 'hund', 'welt']; - const chips = _chipsForWorld(worldNames[_cur]); - const opts = []; - for (const chip of chips) { - if (chip.fab) for (const o of chip.fab) { if (opts.length < 6) opts.push(o); } - } - return opts; - } - function _updateFab() { const fab = document.getElementById('worlds-fab'); if (!fab) return; - const opts = _fabOptions(); - if (!opts.length) { fab.style.display = 'none'; return; } - fab.style.display = ''; - fab.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#paw-print`); - fab.title = 'Schnellaktion'; + const icons = ['note-pencil', 'paw-print', 'warning']; + const titles = ['Schnelleintrag', 'Hund-Eintrag', 'Alarm melden']; + fab.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#${icons[_cur]}`); + fab.title = titles[_cur]; } function _setupButtons() { document.getElementById('worlds-fab')?.addEventListener('click', _openFab); + document.getElementById('worlds-settings')?.addEventListener('click', () => navigateTo('settings')); document.getElementById('worlds-back')?.addEventListener('click', () => show()); document.querySelectorAll('.wdot').forEach((dot, i) => { dot.style.pointerEvents = 'auto'; @@ -206,13 +179,21 @@ window.Worlds = (() => { } function _openFab() { - const options = _fabOptions(); - if (!options.length) return; + const isWelt = _cur === 2; + const dogName = _state?.user ? null : null; // falls mehrere Hunde: erweiterbar - const meldenPages = new Set(['poison','lost','recalls','map']); - const meldenCount = options.filter(o => meldenPages.has(o.page)).length; - const title = meldenCount > options.length / 2 ? 'Was möchtest du melden?' : 'Was möchtest du eintragen?'; + const options = isWelt ? [ + { icon:'skull', color:'#EF4444', label:'Giftköder melden', sub:'Warnung für andere Hundebesitzer', page:'poison' }, + { icon:'dog', color:'#3B82F6', label:'Verlorenen Hund melden', sub:'Hilf beim Wiederfinden', page:'lost' }, + { icon:'map-pin', color:'#10B981', label:'Ort vorschlagen', sub:'Neuen POI auf der Karte', page:'map' }, + ] : [ + { icon:'book-open', color:'#8B5CF6', label:'Tagebucheintrag', sub:'Erlebnis, Foto oder Notiz', page:'diary', action:'openNew' }, + { icon:'target', color:'#F59E0B', label:'Training aufzeichnen',sub:'Übung absolviert', page:'uebungen' }, + { icon:'heartbeat', color:'#EF4444', label:'Tierarztbesuch', sub:'Befund oder Impfung eintragen', page:'health' }, + { icon:'wave-sine', color:'#06B6D4', label:'Gewicht messen', sub:'Aktuelles Gewicht eintragen', page:'health' }, + ]; + // Overlay erstellen const ov = document.createElement('div'); ov.id = 'fab-overlay'; ov.style.cssText = 'position:fixed;inset:0;z-index:300;display:flex;flex-direction:column;justify-content:flex-end'; @@ -222,7 +203,9 @@ window.Worlds = (() => { padding:20px 16px calc(env(safe-area-inset-bottom,16px) + 16px); box-shadow:0 -8px 32px rgba(0,0,0,0.2)">
-
${title}
+
+ ${isWelt ? 'Was möchtest du melden?' : 'Was möchtest du eintragen?'} +
-
- -
Dauer
-
- ${durations.map(d => ` - - `).join('')} -
- - -
- `; - - document.body.appendChild(ov); - - const _close = () => ov.remove(); - ov.querySelector('#qg-backdrop').addEventListener('click', _close); - ov.querySelector('#qg-close').addEventListener('click', _close); - - // Dauer-Toggle - ov.querySelectorAll('.qg-dur').forEach(btn => { - btn.addEventListener('click', () => { - selectedMin = parseInt(btn.dataset.min); - ov.querySelectorAll('.qg-dur').forEach(b => { - const active = parseInt(b.dataset.min) === selectedMin; - b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)'; - b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-bg-card)'; - b.style.color = active ? 'var(--c-primary)' : 'var(--c-text)'; - }); - }); - }); - - // Eintragen - ov.querySelector('#qg-submit').addEventListener('click', async () => { - const submitBtn = ov.querySelector('#qg-submit'); - submitBtn.disabled = true; - submitBtn.textContent = 'Wird eingetragen…'; - - try { - const payload = { - typ: 'gassi', - titel: 'Schnell-Gassi 🐾', - text: `Kurze Runde, ${selectedMin} Minuten`, - }; - if (weatherData) { - payload.weather_json = JSON.stringify(weatherData); - } - await API.post(`/dogs/${dog.id}/diary`, payload); - _close(); - UI.toast?.success(`Gassi eingetragen! ${selectedMin} min 🐾`); - // Streak-Cache invalidieren - try { localStorage.removeItem('w3_streak_' + dog.id); } catch {} - // JETZT-Welt neu rendern für aktuellen Streak - setTimeout(() => _renderJetzt(), 300); - } catch (err) { - submitBtn.disabled = false; - submitBtn.innerHTML = ' Eintragen'; - UI.toast?.error('Fehler beim Eintragen. Bitte erneut versuchen.'); - } - }); - } - // ── CHIP-KONFIGURATION ────────────────────────────────────── // Alle verfügbaren Chips mit Metadaten const _ALL_CHIPS = [ - { icon:'note-pencil', label:'Notizblock', page:'notes', - fab:[{ icon:'note-pencil', color:'#10B981', label:'Neue Notiz', sub:'Schnellnotiz erstellen', page:'notes', action:'openNew' }] }, - { icon:'currency-eur', label:'Ausgaben', page:'expenses', - fab:[{ icon:'currency-eur', color:'#3B82F6', label:'Ausgabe eintragen', sub:'Einmalig oder Dauerauftrag', page:'expenses', action:'openNew' }] }, - { icon:'first-aid', label:'Erste Hilfe', page:'erste-hilfe' }, - { icon:'handshake', label:'Playdate', page:'playdate', - fab:[{ icon:'handshake', color:'#F59E0B', label:'Playdate anfragen', sub:'Treffen mit anderen Hunden', page:'playdate', action:'openNew' }] }, - { icon:'chat-circle-dots', label:'Nachrichten', page:'chat' }, - { icon:'sun', label:'Wetter', page:'wetter' }, + { icon:'note-pencil', label:'Notizblock', page:'notes' }, + { icon:'currency-eur', label:'Ausgaben', page:'expenses' }, + { icon:'first-aid', label:'Erste Hilfe', page:'erste-hilfe' }, + { icon:'handshake', label:'Playdate', page:'playdate' }, + { icon:'chat-circle-dots', label:'Nachrichten', page:'chat' }, + { icon:'sun', label:'Wetter', page:'wetter' }, - { icon:'book-open', label:'Tagebuch', page:'diary', - fab:[{ icon:'book-open', color:'#8B5CF6', label:'Tagebucheintrag', sub:'Erlebnis, Foto oder Notiz', page:'diary', action:'openNew' }] }, - { icon:'heartbeat', label:'Gesundheit', page:'health', - fab:[{ icon:'heartbeat', color:'#EF4444', label:'Tierarztbesuch', sub:'Befund oder Impfung eintragen', page:'health' }, - { icon:'wave-sine', color:'#06B6D4', label:'Gewicht messen', sub:'Aktuelles Gewicht eintragen', page:'health' }] }, - { icon:'target', label:'Übungen', page:'uebungen', - fab:[{ icon:'target', color:'#F59E0B', label:'Training aufzeichnen', sub:'Übung absolviert', page:'uebungen' }] }, - { icon:'list-checks', label:'Trainings-\npläne', page:'trainingsplaene', - fab:[{ icon:'list-checks', color:'#10B981', label:'Plan erstellen', sub:'Neuen Trainingsplan anlegen', page:'trainingsplaene', action:'openNew' }] }, - { icon:'heart', label:'Adoption', page:'adoption', - fab:[{ icon:'heart', color:'#EF4444', label:'Hund anbieten', sub:'Zur Adoption freigeben', page:'adoption', action:'openNew' }] }, - { icon:'house-line', label:'Sitting', page:'sitting', - fab:[{ icon:'house-line', color:'#8B5CF6', label:'Sitter anfragen', sub:'Betreuung buchen', page:'sitting', action:'openNew' }] }, - { icon:'books', label:'Wiki', page:'wiki' }, - { icon:'scales', label:'Wurfbörse', page:'wurfboerse' }, - { icon:'map-trifold', label:'Karte', page:'map', - fab:[{ icon:'map-pin', color:'#10B981', label:'Ort vorschlagen', sub:'Neuen POI auf der Karte', page:'map' }] }, - { icon:'push-pin', label:'Forum', page:'forum', - fab:[{ icon:'push-pin', color:'#8B5CF6', label:'Forum-Beitrag', sub:'Thema oder Frage erstellen', page:'forum', action:'openNew' }] }, - { icon:'users', label:'Freunde', page:'friends', - fab:[{ icon:'users', color:'#3B82F6', label:'Freund einladen', sub:'Per Link einladen', page:'friends', action:'openNew' }] }, - { icon:'paw-print', label:'Gassi', page:'walks', - fab:[{ icon:'paw-print', color:'#F59E0B', label:'Gassirunde', sub:'Neue Runde starten', page:'walks', action:'openNew' }, - { icon:'paw-print', color:'#10B981', label:'Schnell-Gassi', sub:'Kurze Runde ohne GPS eintragen', page:'walks', action:'quickGassi' }] }, - { icon:'skull', label:'Giftköder', page:'poison', - fab:[{ icon:'skull', color:'#EF4444', label:'Giftköder melden', sub:'Warnung für andere Hundebesitzer', page:'poison' }] }, - { icon:'warning-circle', label:'Rückrufe', page:'recalls', - fab:[{ icon:'warning-circle', color:'#EF4444', label:'Rückruf melden', sub:'Produkt oder Futter', page:'recalls', action:'openNew' }] }, - { icon:'dog', label:'Verlorene', page:'lost', - fab:[{ icon:'dog', color:'#3B82F6', label:'Verlorenen melden', sub:'Hilf beim Wiederfinden', page:'lost' }] }, - { icon:'path', label:'Routen', page:'routes', - fab:[{ icon:'path', color:'#10B981', label:'Route aufzeichnen', sub:'GPS-Tracking starten', page:'routes', action:'openNew' }] }, - { icon:'calendar-dots', label:'Events', page:'events', - fab:[{ icon:'calendar-dots', color:'#06B6D4', label:'Event erstellen', sub:'Veranstaltung ankündigen', page:'events', action:'openNew' }] }, - { icon:'sparkle', label:'Jobs', page:'jobs' }, - { icon:'book-open', label:'Knigge', page:'knigge' }, - { icon:'film-slate', label:'Filme', page:'movies' }, - { icon:'tree-structure', label:'Zucht-\nkartei', page:'zuchthunde', role:'breeder', - fab:[{ icon:'tree-structure', color:'#8B5CF6', label:'Zuchthund eintragen', sub:'Neuen Hund anlegen', page:'zuchthunde', action:'openNew' }] }, - { icon:'notebook', label:'Wurfverw.', page:'litters', role:'breeder', - fab:[{ icon:'notebook', color:'#10B981', label:'Wurf anlegen', sub:'Neuen Wurf eintragen', page:'litters', action:'openNew' }] }, - { icon:'sparkle', label:'Social', page:'social', role:'social', - fab:[{ icon:'sparkle', color:'#EC4899', label:'Social-Post', sub:'Beitrag erstellen', page:'social', action:'openNew' }] }, - { icon:'shield-check', label:'Moderation', page:'moderation', role:'mod' }, - { icon:'gear', label:'Admin', page:'admin', role:'admin' }, + { icon:'book-open', label:'Tagebuch', page:'diary' }, + { icon:'heartbeat', label:'Gesundheit', page:'health' }, + { icon:'target', label:'Übungen', page:'uebungen' }, + { icon:'list-checks', label:'Trainings-\npläne',page:'trainingsplaene'}, + { icon:'heart', label:'Adoption', page:'adoption' }, + { icon:'house-line', label:'Sitting', page:'sitting' }, + { icon:'books', label:'Wiki', page:'wiki' }, + { icon:'scales', label:'Wurfbörse', page:'wurfboerse' }, + { icon:'map-trifold', label:'Karte', page:'map' }, + { icon:'push-pin', label:'Forum', page:'forum' }, + { icon:'users', label:'Freunde', page:'friends' }, + { icon:'paw-print', label:'Gassi', page:'walks' }, + { icon:'skull', label:'Giftköder', page:'poison' }, + { icon:'warning-circle', label:'Rückrufe', page:'recalls' }, + { icon:'dog', label:'Verlorene', page:'lost' }, + { icon:'path', label:'Routen', page:'routes' }, + { icon:'calendar-dots', label:'Events', page:'events' }, + { icon:'sparkle', label:'Jobs', page:'jobs' }, + { icon:'book-open', label:'Knigge', page:'knigge' }, + { icon:'film-slate', label:'Filme', page:'movies' }, + { icon:'tree-structure', label:'Zucht-\nkartei', page:'zuchthunde', role:'breeder' }, + { icon:'notebook', label:'Wurfverw.', page:'litters', role:'breeder' }, + { icon:'sparkle', label:'Social', page:'social', role:'social' }, + { icon:'shield-check', label:'Moderation', page:'moderation', role:'mod' }, + { icon:'gear', label:'Admin', page:'admin', role:'admin' }, ]; const _DEFAULT_CONFIG = { @@ -460,40 +296,12 @@ window.Worlds = (() => { welt: ['map','forum','friends','walks','poison','recalls','lost','routes','events','jobs','knigge','movies'], }; - // _cfgCache: wird beim Init aus DB geladen, Fallback localStorage → Default - let _cfgCache = null; - - async function _loadConfigFromServer() { - try { - const res = await API.get('/profile/world-config'); - if (res?.config) { - _cfgCache = res.config; - try { localStorage.setItem('world_chips', JSON.stringify(_cfgCache)); } catch {} - return; - } - // Noch nichts in DB: lokale Config hochladen (einmalige Migration) - const local = (() => { try { return JSON.parse(localStorage.getItem('world_chips') || 'null'); } catch { return null; } })(); - if (local) { - _cfgCache = local; - API.put('/profile/world-config', { config: local }).catch(() => {}); - return; - } - } catch {} - // Fallback: localStorage → Default - try { _cfgCache = JSON.parse(localStorage.getItem('world_chips') || 'null') || _DEFAULT_CONFIG; } - catch { _cfgCache = _DEFAULT_CONFIG; } - } - function _getConfig() { - return _cfgCache || _DEFAULT_CONFIG; + try { return JSON.parse(localStorage.getItem('world_chips') || 'null') || _DEFAULT_CONFIG; } + catch { return _DEFAULT_CONFIG; } } - function _saveConfig(cfg) { - _cfgCache = cfg; try { localStorage.setItem('world_chips', JSON.stringify(cfg)); } catch {} - if (_state?.user) { - API.put('/profile/world-config', { config: cfg }).catch(() => {}); - } } function _chipMeta(page) { return _ALL_CHIPS.find(c => c.page === page) || null; @@ -766,13 +574,18 @@ window.Worlds = (() => { async function _loadDailyImage(dog) { if (!dog) return null; - const todayKey = 'bg3_' + new Date().toISOString().slice(0, 10); + const todayKey = 'bg_' + new Date().toISOString().slice(0, 10); const cached = _wLoad(todayKey); if (cached?.data) return cached.data; try { - const dash = await API.dogs.welcomeDashboard(dog.id); - const url = dash?.random_photo?.url || dog.foto_url || null; - if (url) _wSave(todayKey, url); + const r = await _cachedGet(`diary_${dog.id}`, `/dogs/${dog.id}/diary?limit=30`); + const entries = r.data?.entries || r.data || []; + const withPhotos = entries.filter(e => (e.foto_urls?.length || e.foto_url)); + if (!withPhotos.length) { const u = dog.foto_url || null; if(u) _wSave(todayKey, u); return u; } + const day = Math.floor(Date.now() / 86400000); + const entry = withPhotos[day % withPhotos.length]; + const url = (entry.foto_urls?.[0] || entry.foto_url); + _wSave(todayKey, url); return url; } catch { return dog.foto_url || null; } } @@ -812,11 +625,10 @@ window.Worlds = (() => { const user = _state?.user; el.innerHTML = _skeleton(3); - const [weatherRes, dogsRes, alertsRes, achRes] = await Promise.allSettled([ + const [weatherRes, dogsRes, alertsRes] = await Promise.allSettled([ _getCachedWeather(), user ? _cachedGet('dogs', '/dogs') : Promise.resolve({ data: [], fromCache: false, ageMin: 0 }), user ? _getNearbyAlerts() : Promise.resolve([]), - user ? _cachedGet('achievements_me', '/achievements/me') : Promise.resolve({ data: null }), ]); const weatherObj = weatherRes.value || { data: null, fromCache: false, ageMin: 0 }; @@ -825,7 +637,6 @@ window.Worlds = (() => { const dogList = dogsObj.data || []; const dog = dogList[0] || null; const alertList = alertsRes.value || []; - const totalKm = achRes.value?.data?.stats?.total_km ?? null; const isOffline = weatherObj.fromCache && dogsObj.fromCache; const staleMin = Math.max(weatherObj.ageMin || 0, dogsObj.ageMin || 0); @@ -901,7 +712,7 @@ window.Worlds = (() => {
${_esc(greet)}${firstName ? `, ${_esc(firstName)}` : ''}${stale}
-
${_esc(dayStr)}${weatherLine ? ' · ' + weatherLine : ''}${totalKm != null ? ' · ' + totalKm + ' km' : ''}
+
${_esc(dayStr)}${weatherLine ? ' · ' + weatherLine : ''}
${user ? userAvatarHtml : ''} diff --git a/backend/static/sw.js b/backend/static/sw.js index 883e797..e786916 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-v700'; +const CACHE_VERSION = 'by-v651'; 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 @@ -202,8 +202,7 @@ self.addEventListener('fetch', event => { .then(resp => { if (resp.ok) { _cacheMark(url.pathname); - const toCache = resp.clone(); - caches.open(CACHE_API).then(c => c.put(event.request, toCache)); + caches.open(CACHE_API).then(c => c.put(event.request, resp.clone())); } return resp; }) diff --git a/backend/weather.py b/backend/weather.py index 5e836c6..e5f2317 100644 --- a/backend/weather.py +++ b/backend/weather.py @@ -222,7 +222,6 @@ async def get_forecast(lat: float, lon: float) -> dict: "apparent_temperature_min,precipitation_probability_max,precipitation_sum," "weathercode,windspeed_10m_max,winddirection_10m_dominant,uv_index_max," "sunrise,sunset" - "&hourly=precipitation_probability,precipitation,weathercode" "&timezone=auto&forecast_days=7" ) pollen_url = ( @@ -246,7 +245,6 @@ async def get_forecast(lat: float, lon: float) -> dict: raw = forecast_resp.json() daily = raw.get('daily', {}) - hourly_fc = raw.get('hourly', {}) timezone = raw.get('timezone', 'auto') dates = daily.get('time', []) @@ -263,24 +261,6 @@ async def get_forecast(lat: float, lon: float) -> dict: sunrises = daily.get('sunrise', []) sunsets = daily.get('sunset', []) - # --- Hourly precipitation data grouped by day --- - hourly_times = hourly_fc.get('time', []) - hourly_pp = hourly_fc.get('precipitation_probability', []) - hourly_precip = hourly_fc.get('precipitation', []) - hourly_wcode = hourly_fc.get('weathercode', []) - # Build: date_str → list of {hour, precip_prob, precip, weathercode} - _hourly_by_day: dict = {} - for idx, ts_str in enumerate(hourly_times): - day_str = ts_str[:10] # 'YYYY-MM-DD' - hour_str = ts_str[11:16] # 'HH:MM' - entry = { - 'hour': hour_str, - 'precip_prob': hourly_pp[idx] if idx < len(hourly_pp) else None, - 'precip': hourly_precip[idx] if idx < len(hourly_precip) else None, - 'weathercode': int(hourly_wcode[idx]) if idx < len(hourly_wcode) and hourly_wcode[idx] is not None else None, - } - _hourly_by_day.setdefault(day_str, []).append(entry) - # --- Pollen (optional) --- pollen_daily: dict | None = None if not isinstance(pollen_resp, Exception): @@ -381,7 +361,6 @@ async def get_forecast(lat: float, lon: float) -> dict: 'zecken': zecken, 'thunderstorm': wcode in {95, 96, 99}, 'paw_cold': wcode in {71, 73, 75, 77, 85, 86} or (t_min is not None and t_min < 0), - 'hourly': _hourly_by_day.get(date_str, []), }) result = {'timezone': timezone, 'days': days} diff --git a/scripts/dog_quotes.json b/scripts/dog_quotes.json deleted file mode 100644 index 7c3c8e2..0000000 --- a/scripts/dog_quotes.json +++ /dev/null @@ -1,348 +0,0 @@ -[ - { "text": "Der Hund ist des Menschen bester Freund.", "autor": "Sprichwort", "kategorie": "allgemein" }, - { "text": "Ein treuer Hund ist besser als ein falscher Freund.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde haben alle guten Eigenschaften des Menschen, ohne gleichzeitig seine Fehler zu besitzen.", "autor": "Friedrich II.", "kategorie": "weisheit" }, - { "text": "Dem Hunde, wenn er gut erzogen, wird selbst ein weiser Mann gewogen.", "autor": "Johann Wolfgang von Goethe", "kategorie": "training" }, - { "text": "Die Treue eines Hundes ist ein kostbares Geschenk, das nicht minder bindende moralische Verpflichtungen auferlegt als die Freundschaft eines Menschen.", "autor": "Konrad Lorenz", "kategorie": "treue" }, - { "text": "Wenn du einen verhungerten Hund aufliest und machst ihn satt, dann wird er dich nicht beißen. Das ist der Grundunterschied zwischen Hund und Mensch.", "autor": "Mark Twain", "kategorie": "weisheit" }, - { "text": "Woran sollte man sich von der endlosen Verstellung, Falschheit und Heimtücke der Menschen erholen, wenn die Hunde nicht wären, in deren ehrliches Gesicht man ohne Misstrauen schauen kann?", "autor": "Arthur Schopenhauer", "kategorie": "weisheit" }, - { "text": "Ein Leben ohne Hund ist ein Irrtum.", "autor": "Carl Zuckmayer", "kategorie": "allgemein" }, - { "text": "Man kann auch ohne Hund leben, aber es lohnt sich nicht.", "autor": "Heinz Rühmann", "kategorie": "allgemein" }, - { "text": "Ein Hund ist das Einzige auf der Welt, das dich mehr liebt als sich selbst.", "autor": "Josh Billings", "kategorie": "liebe" }, - { "text": "In den Augen meines Hundes liegt mein ganzes Glück, all mein Inneres, Krankes, Wundes heilt in seinem Blick.", "autor": "Friederike Kempner", "kategorie": "liebe" }, - { "text": "Hunde sind nicht unser ganzes Leben, aber sie machen unser Leben ganz.", "autor": "Roger Caras", "kategorie": "liebe" }, - { "text": "Mein kleiner Hund — ein Herzschlag an meinen Füßen.", "autor": "Edith Wharton", "kategorie": "liebe" }, - { "text": "Der Hund ist ein Ehrenmann. Ich hoffe, in seinen Himmel zu kommen, nicht in den des Menschen.", "autor": "Mark Twain", "kategorie": "humor" }, - { "text": "Wenn du dich einsam fühlst, kaufe dir keinen Freund — leih dir einen Hund.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Ein Hund, der bellt, ist mehr wert als ein Mensch, der lügt.", "autor": "Henry de Montherlant", "kategorie": "weisheit" }, - { "text": "Ich hatte lieber meinen Hund bellen hören als einen Mann schwören, er liebe mich.", "autor": "William Shakespeare", "kategorie": "weisheit" }, - { "text": "Hunde sind die Führer des Planeten. Wenn du zwei Lebewesen siehst, von denen eins einen Haufen macht und das andere ihn aufhebt — wer ist wohl der Chef?", "autor": "Jerry Seinfeld", "kategorie": "humor" }, - { "text": "Ein Hund bringt dir Stöcke, weil er denkt, du mochtest ihn so sehr, dass du ihn sofort weggeworfen hast.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Hunde lehren uns eine Menge über das Leben: Treue, Ausdauer und vor dem Hinlegen dreimal im Kreis zu drehen.", "autor": "Robert Benchley", "kategorie": "humor" }, - { "text": "Ich arbeite hart, damit mein Hund ein besseres Leben führen kann.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Wer sagt, dass Geld nicht glücklich macht, hat noch nie ein weinendes Kind gesehen, dem gerade ein Welpe geschenkt wurde.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Das Schönste, was ein Mensch besitzen kann, ist die Liebe eines Hundes.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund ist der einzige Freund, den du kaufen kannst.", "autor": "Sprichwort", "kategorie": "allgemein" }, - { "text": "Hundehaare sind das ultimative Accessoire für jede Kleidung.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Wenn dein Hund übergewichtig ist, dann warst du nicht genug an der frischen Luft.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Gassi gehen ist keine Pflicht — es ist ein Abenteuer.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Ein müder Hund ist ein guter Hund.", "autor": "Sprichwort", "kategorie": "training" }, - { "text": "Hunde haben mehr Liebe zu verschenken als die meisten Menschen.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Kein Psychiater der Welt kann einem Menschen so helfen wie ein Hund.", "autor": "Bern Williams", "kategorie": "liebe" }, - { "text": "Den eigenen Fokus auf erwünschtes Verhalten zu richten anstatt auf das, was nicht klappt, ist nicht nur für die Hundeerziehung ein großartiger Ansatz.", "autor": "Christina Hanf", "kategorie": "training" }, - { "text": "Die Menschen erwarten zu viel von ihrem Hund und zu wenig von sich selbst.", "autor": "Bob Bailey", "kategorie": "training" }, - { "text": "Gewalt bewirkt niemals etwas Gutes, weder bei Menschen noch bei Hunden.", "autor": "Turid Rugaas", "kategorie": "training" }, - { "text": "Training bedeutet, Situationen zu schaffen, in denen der Hund viel richtig machen kann.", "autor": "Gudrun Scholz", "kategorie": "training" }, - { "text": "Hunde erziehen heißt, sich selbst zu erziehen.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Ein gut erzogener Hund macht seinem Besitzer Ehre.", "autor": "Sprichwort", "kategorie": "training" }, - { "text": "Lass schlafende Hunde liegen.", "autor": "Sprichwort", "kategorie": "weisheit" }, - { "text": "Wenn ein alter Hund bellt, soll man hinausschauen.", "autor": "Sprichwort", "kategorie": "weisheit" }, - { "text": "Der Hund, der viel bellt, beißt selten.", "autor": "Sprichwort", "kategorie": "weisheit" }, - { "text": "Beißt dich ein Hund, zeig ihm nicht die Wunde.", "autor": "Sprichwort", "kategorie": "weisheit" }, - { "text": "Mit Hunden und Kindern kommt man immer gut an.", "autor": "Sprichwort", "kategorie": "allgemein" }, - { "text": "Streichle einen Hund und er gehört dir fürs Leben.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Ein Hund vergisst keine Freundlichkeit.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde kennen keine Vorurteile.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund liebt dich, egal wie du aussiehst oder was du hast.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde sind Meister darin, im Augenblick zu leben.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Wenn Menschen so treu wären wie Hunde, wäre die Welt eine bessere.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Der Blick eines Hundes kann mehr sagen als tausend Worte.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Jeder Mensch verdient einen Hund. Umgekehrt ist auch wahr: Jeder Hund verdient einen guten Menschen.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Ein Hund weiß nicht, was Einsamkeit bedeutet — er ist immer bei dir.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde lieben bedingungslos — etwas, das wir Menschen erst lernen müssen.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Das Leben ist zu kurz für schlechtes Hundefutter.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Wer täglich mit seinem Hund spaziert, lebt länger und glücklicher.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Bewegung ist das beste Medikament — für Hund und Mensch.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Ein Hund an der Leine hält auch sein Herrchen fit.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Laufen mit dem Hund ist die schönste Form des Sports.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Hundebesitzer gehen Gassi. Hunde hingegen gehen auf eine Geruchssafari.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Immer wenn du beim Gassi gehen auf dein Handy schaust, verpasst du ein Abenteuer mit deinem Hund.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Die beste Therapie hat vier Pfoten und einen Schwanz.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde bringen das Beste in uns zum Vorschein.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Wer Hunde hat, hat selten Feinde.", "autor": "Sprichwort", "kategorie": "allgemein" }, - { "text": "Hunde verstehen uns oft besser als wir uns selbst.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund hat kein Ego — dafür hat er alles andere.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "The fidelity of a dog is a precious gift demanding no less binding moral responsibilities than the friendship of a human being.", "autor": "Konrad Lorenz", "kategorie": "treue" }, - { "text": "Dogs are not our whole life, but they make our lives whole.", "autor": "Roger Caras", "kategorie": "liebe" }, - { "text": "A dog is the only thing on earth that loves you more than he loves himself.", "autor": "Josh Billings", "kategorie": "liebe" }, - { "text": "Happiness is a warm puppy.", "autor": "Charles M. Schulz", "kategorie": "liebe" }, - { "text": "The better I get to know men, the more I find myself loving dogs.", "autor": "Charles de Gaulle", "kategorie": "humor" }, - { "text": "No matter how little money and how few possessions you own, having a dog makes you rich.", "autor": "Louis Sabin", "kategorie": "liebe" }, - { "text": "My goal in life is to be as good of a person as my dog already thinks I am.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Dogs have a way of finding the people who need them.", "autor": "Thom Jones", "kategorie": "liebe" }, - { "text": "A dog will teach you unconditional love. If you can have that in your life, things won't be too bad.", "autor": "Robert Wagner", "kategorie": "liebe" }, - { "text": "Ein Hund ist ein Teil unseres Lebens. Für unseren Hund sind wir sein ganzes Leben.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde geben mehr als sie nehmen — das können die wenigsten von sich behaupten.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Der größte Fehler beim Hundetraining ist, die Geduld zu vergessen.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Belohnung schafft Vertrauen — Strafe schafft Angst.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Positive Verstärkung ist der Schlüssel zu einer glücklichen Hund-Mensch-Beziehung.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Ein Hund, der Freude am Lernen hat, ist ein Hund, der Freude am Leben hat.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Konsequenz bedeutet nicht Strenge — es bedeutet Verlässlichkeit.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Ein Hund braucht Grenzen, so wie ein Kind Grenzen braucht — aus Liebe, nicht aus Macht.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Hunde folgen nicht den Gesetzen der Logik — sie folgen den Gesetzen des Herzens.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Wenn du deinen Hund liebst, zeig es — er wartet den ganzen Tag darauf.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Kein Haus ist groß genug für einen Hund und seine Haare.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Früher hatten wir ein schönes Sofa. Jetzt hat unser Hund ein schönes Sofa.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Mein Hund und ich haben eine Vereinbarung: Ich tue so, als würde ich das Essen nicht mitessen wollen, und er tut so, als würde er es glauben.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Hundeliebe ist die reinste Form der Liebe — sie fragt nichts und gibt alles.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund ist der einzige Zeuge, der keine Erwartungen an dich stellt.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Mit einem Hund an deiner Seite bist du nie wirklich allein.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Der Schwanz eines Hundes lügt nie.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde wissen genau, wann du traurig bist — und sie entscheiden sich trotzdem, bei dir zu bleiben.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Wenn ein Hund dir vertraut, hat er dir sein Herzstück gegeben.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde mögen alle Menschen, die sie kennen — bis auf den Tierarzt. Und selbst den vergeben sie.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Gib deinem Hund heute ein extra Leckerli — er hat es sich verdient, einfach indem er da ist.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund ist das lebende Beispiel dafür, dass man mit nichts als Liebe reich sein kann.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde bellen, die Karawane zieht weiter.", "autor": "Sprichwort", "kategorie": "weisheit" }, - { "text": "Wer einmal einen Hund gehabt hat, kann sich ein Leben ohne nicht mehr vorstellen.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Hundehaare auf dem Sofa sind kein Problem — sie sind ein Zeichen von Liebe.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ein Hund ist kein Tier — er ist ein Familienmitglied.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Das Einzige, was ein Hund von dir will, ist deine Zeit.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde lachen auch — nur mit dem Schwanz.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ein Hund trauert nie um eine verpasste Chance — er schafft sofort die nächste.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde leben im Moment. Das sollten wir auch.", "autor": "Cesar Millan", "kategorie": "weisheit" }, - { "text": "Dein Hund braucht kein Geschenk zu deinem Geburtstag — für ihn ist jeder Tag mit dir ein Fest.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund riecht deine Stimmung, bevor du sie selbst bemerkst.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde kennen keine schlechten Tage — sie kennen nur schlechte Momente, die mit einem Kuss besser werden.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Jedem Hundebesitzer passiert es einmal: Er redet mit seinem Hund als wäre er ein Mensch. Dem Hund ist das egal — er hört einfach zu.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Hunde sind die einzigen Wesen, die dich für das lieben, was du bist, nicht für das, was du hast.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein gut erzogener Hund ist das Ergebnis eines gut erzogenen Besitzers.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Im Frühling, wenn die Erde sich erneuert, beginnt auch der Hund sein Fell zu verlieren — liberal.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Treu wie ein Hund — das ist kein Vergleich, das ist ein Kompliment.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Wer einen Hund streichelt, streichelt auch sich selbst.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Hunde senken den Blutdruck — zumindest bis sie auf dem weißen Teppich kotzen.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Studien zeigen: Hundebesitzer haben ein gesünderes Herz. Kein Wunder — ihr Herz wird täglich trainiert.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Gassi gehen bei Regen ist kein Spaß — aber dein Hund findet es toll.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Der einzige Fehler, den ein Hund macht, ist, zu früh zu sterben.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde machen keine Politik. Deswegen sind sie klüger als wir.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ein Haus wird zum Zuhause, wenn ein Hund darin wohnt.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Der Hund ist der älteste Freund des Menschen — und noch immer der beste.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Wer einen Hund hat, hat keinen Grund zu klagen — höchstens über Hundehaare.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Manche Menschen verdienen nicht den Hund, den sie haben.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein guter Mensch behandelt seinen Hund wie einen Freund.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Wo ein Hund ist, ist Freude.", "autor": "Sprichwort", "kategorie": "allgemein" }, - { "text": "Ein Hund weiß nicht, was Gleichgültigkeit ist.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde erkennen gute Menschen auf Anhieb.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Vertraue dem Urteil deines Hundes über andere Menschen.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund, der dir ohne Hintergedanken folgt, ist ein Geschenk.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hundeerziehung ist Beziehungspflege.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Wenn du deinen Hund gut kennst, kennt er dich noch besser.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Das schönste Geräusch am Morgen: der Schwanz deines Hundes, der wedelt, weil du aufgewacht bist.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund redet nicht — und sagt doch alles.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde haben keine Worte für Enttäuschung — nur für Freude.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Für deinen Hund bist du der Beste — immer, ohne Ausnahme.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Der Hund ist das einzige Tier, das den Menschen als seinen Gott betrachtet.", "autor": "George Bernard Shaw", "kategorie": "weisheit" }, - { "text": "Bis es keinen Hund in meinem Leben gibt, wird mein Herz nie ganz gefüllt sein.", "autor": "Roger Caras", "kategorie": "liebe" }, - { "text": "Hunde sind die Philosophen des Alltags — sie leben, ohne zu grübeln.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Der weise Hund macht einen Bogen um den Stock.", "autor": "Sprichwort", "kategorie": "weisheit" }, - { "text": "Hunde laufen auf vier Beinen und tragen dennoch die ganze Welt auf dem Rücken ihrer Besitzer.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund ist der Beweis, dass die Natur manchmal Vollkommenes erschafft.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Hunde haben die Weisheit, den Menschen dort zu treffen, wo er steht.", "autor": "Orhan Pamuk", "kategorie": "weisheit" }, - { "text": "Für meinen Hund bin ich kein Held, kein Versager — ich bin einfach sein Zuhause.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Der Hund ist das Symbol der Loyalität, weil er sie nicht kennt — er lebt sie.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Ein Hund bellte einmal einen Dieb an — und Menschen nennen ihn seitdem treu.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Wer mit Hunden liegt, steht mit Flöhen auf.", "autor": "Sprichwort", "kategorie": "humor" }, - { "text": "Zwei Dinge machen das Leben leichter: ein guter Kaffee und ein treuer Hund.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Der Hund liebt die Wahrheit — er ist das ehrlichste Lebewesen auf dem Planeten.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Es gibt keine schlechten Hunde. Nur schlechte Erziehung.", "autor": "Cesar Millan", "kategorie": "training" }, - { "text": "Ein Hund ist glücklich, wenn er beschäftigt ist und weiß, was von ihm erwartet wird.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Lob kostet nichts und bedeutet dem Hund alles.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Strafe zeigt dem Hund nur, was er nicht soll — Belohnung zeigt ihm, was er soll.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Ein entspannter Hund ist ein trainierter Hund.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Jede Rasse hat ihre Eigenheiten — der gute Hundetrainer kennt sie alle.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Wenn du willst, dass dein Hund gehorcht, musst du zuerst lernen zu verstehen.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Training ist Dialog, nicht Monolog.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Ein gelangweilter Hund ist ein Problemhund.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Konsequenz beim Training bedeutet: Heute gilt dasselbe wie gestern.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Der beste Hundetrainer ist der, dem der Hund freiwillig folgt.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Hunde lernen durch Wiederholung und Belohnung — nicht durch Zwang.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Körpersprache ist alles im Hundetraining — was du sagst, ist weniger wichtig als wie du es sagst.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Ein Hund verzeiht Fehler schneller als jeder Mensch.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde halten keine Groll — sie lieben weiter, egal was war.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Treue ist keine Eigenschaft — sie ist eine Entscheidung. Hunde haben sie nie bereut.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Ein Hund folgt dir in die Dunkelheit, ohne zu zögern.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Die Treue eines Hundes beschämt uns manchmal, weil wir so selten dasselbe bieten.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Ein Hund, der wartet, zeigt mehr Liebe als Worte ausdrücken können.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Der japanische Hund Hachiko wartete neun Jahre auf seinen Herrn. Frage dich, wie lange du warten würdest.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Kein anderes Wesen auf der Welt ist so treu wie ein Hund.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde wählen ihre Besitzer nicht aus — und lieben sie trotzdem.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Ein Hund bleibt bei dir, wenn alle anderen gehen.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Wenn dein Hund dir seinen Bauch zeigt, schenkt er dir das größte Vertrauen, das ein Tier geben kann.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde lieben ohne zu vergleichen — das können Menschen selten.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Der Atem deines Hundes im Nacken bedeutet: Du bist nicht allein.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Nichts heilt einen schlechten Tag schneller als ein Hund, der dir entgegenläuft.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde sind das liebste Geheimnis glücklicher Menschen.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund macht Liebe sichtbar.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Wenn du einen Hund streichelst, streichelt er gleichzeitig deine Seele.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hundeaugen lügen nicht — dort siehst du reine Liebe.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Die Liebe eines Hundes ist keine Anhänglichkeit — sie ist Hingabe.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund liebt dich an deinen schlechtesten Tagen am meisten.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Mit einem Hund zu schlafen ist die wärmste Art, eine Nacht zu verbringen.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Wenn Hunde beten könnten, würden sie für ihre Menschen beten.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund braucht nichts außer dir — und das ist sein Reichtum.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Liebe eines Hundes zu verdienen ist die einfachste und gleichzeitig die schönste Sache der Welt.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Wer noch nie von einem Hund geliebt wurde, kennt ein ganzes Stück Glück nicht.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde wissen, dass Liebe keine Bedingungen braucht.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hundewelpen sind die beste Anti-Depressions-Therapie der Welt — leider nicht von der Krankenkasse erstattet.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Mein Hund hat mir beigebracht, ruhig zu sein. Schade, dass er selbst nie ruhig ist.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Manche sagen: Es ist nur ein Hund. Diese Menschen haben nie einen Hund gehabt.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Der einzige Nachteil eines Hundes: Er lebt zu kurz.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Wenn Hunde reden könnten, würden sie uns sagen, was wir schon immer wissen wollten: dass wir gut genug sind.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde schlafen 18 Stunden am Tag und sehen dabei entspannter aus als ich nach 8 Stunden Schlaf.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Mein Hund hat fünf Betten und schläft trotzdem auf meinen Füßen.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ein Hund frisst alles außer dem, was im Napf ist.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Mein Hund hat mehr Freunde auf Instagram als ich — und postet nie selbst.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Hunde kennen kein Montag-Gefühl.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Kein Hund schaut je auf die Uhr — höchstens zur Futterzeit.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Der Traumjob wäre, bezahlt zu werden, während der Hund auf dem Schoß liegt.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Hundebesitzer kennen zwei Arten von Böden: sauber und nach dem Gassi.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Mein Hund ist offiziell der Einzige, der mich ohne Make-up schön findet.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ein Hund ist der beste Grund, früh aufzustehen.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Hunde zeigen uns, wie schön das Einfache ist: Spaziergang, Leckerli, Kuscheln.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Kein Hund hat je Krieg geführt.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde verbringen kein Leben damit zu bereuen. Sie leben einfach.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Die Weisheit des Hundes liegt darin, dass er nie zweifelt, ob er genug tut.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund hat keine Vergangenheit, die er bereut, und keine Zukunft, die er fürchtet.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde leben nicht nach Regeln — sie leben nach Instinkten. Das ist manchmal weiser.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "In der Stille eines Hundes liegt mehr Weisheit als in manchem langen Gespräch.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund versteht mehr von Empathie als jeder Therapeut.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde haben das, was wir suchen: Zufriedenheit ohne Grund.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Die wahre Weisheit des Hundes: Er braucht nichts zu beweisen.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund sieht in dir keinen Status, kein Einkommen, keine Vergangenheit — nur dich.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde wählen Menschen nicht nach Äußerlichkeiten — das sollten wir auch nicht.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Achte darauf, wie dein Hund Menschen begegnet — er spürt, was du nicht siehst.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Wenn dein Hund dir nicht traut, solltest du in dich gehen.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Der Hund ist das beste Argument für die Existenz von Güte in der Welt.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde spiegeln wider, wer wir wirklich sind.", "autor": "Cesar Millan", "kategorie": "weisheit" }, - { "text": "Wenn ich an die Treulosigkeit der Menschen denke, lobe ich meinen Hund.", "autor": "Friedrich II.", "kategorie": "treue" }, - { "text": "Ein Hund wird nie sagen: Heute habe ich keine Lust, dein Freund zu sein.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde verlassen uns nie — wir verlassen sie.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Ein Hund hat kein Gedächtnis für Groll — nur für Zuneigung.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Wenn ein Hund dir treu ist, hat er eine Entscheidung für sein Leben getroffen.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Treue ist nicht, was ein Hund tut — es ist, wer er ist.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Ein Hund trägt kein Netz aus Erwartungen — nur das Netz der Zuneigung.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Wenn Menschen so wären wie Hunde — treu, ehrlich, freudig — wäre die Welt ein besserer Ort.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde zeigen dir täglich, dass Loyalität keine Leistung ist, sondern ein Charakter.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde liegen zu Füßen der Helden und der Hilflosen gleichermaßen.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Tägliche Bewegung mit dem Hund ist der günstigste Arztbesuch, den du machen kannst.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Ein Hund hält dich körperlich fit — ob du willst oder nicht.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Zehn Minuten mit einem Hund spielen senkt Stress nachweislich.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Hundebesitzer haben im Durchschnitt einen niedrigeren Blutdruck. Kein Wunder.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Ein Hund motiviert dich, das Sofa zu verlassen — jeden Tag, bei jedem Wetter.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Wer täglich Gassi geht, braucht kein Fitnessstudio.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Hunde halten uns jung — zumindest im Herzen.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Ein Hund an der Seite hilft gegen Depressionen besser als viele Tabletten.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Streicheln ist gut für den Menschen und noch besser für den Hund.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Hunde sind die natürlichste Form der Stresstherapie.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Mit einem Hund draußen zu sein bedeutet: frische Luft, Bewegung, Freude — dreifacher Gewinn.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Ein gesunder Hund ist ein aktiver Hund — und ein aktiver Hund braucht einen aktiven Menschen.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Hunde sorgen dafür, dass wir uns bewegen — das ist ihr heimliches Gesundheitsprogramm für uns.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Der Hund kennt das Geheimnis des Glücks: Das Beste ist immer das Jetzt.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde fragen nicht, was das Leben bedeutet. Sie leben es einfach.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Wenn du nicht weißt, wohin — folge deinem Hund.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ein Hund zeigt dir jeden Tag: Kleine Dinge sind die wichtigsten.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde tanzen nicht, wenn du ihnen von deinen Erfolgen erzählst — sie tanzen, wenn du heimkommst.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Das Gebell eines Hundes ist die ehrlichste Meinung der Welt.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund hat keine Agenda außer Liebe.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde suchen nicht nach dem Sinn des Lebens — sie finden ihn im Spaziergang.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund macht dich vollständig — auch wenn du das erst merkst, wenn er weg ist.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Im Trauerfall ist ein Hund oft der einzige Trost, der nicht nach Worten sucht.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde lieben jeden Tag so, als wäre es der schönste ihres Lebens.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Für einen Hund ist Heimkommen das größte Ereignis des Tages — täglich.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund jubelt bei deiner Ankunft mehr als dein Telefon bei einem Like.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde misstrauen keinem ohne Grund. Lerne davon.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Kein Hund hat je einen Krieg begonnen oder ein Tier beleidigt ohne Grund.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein treuer Hund ist ein Spiegel des Guten in der Welt.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Liebe, die nicht urteilt — das ist Hundeliebe.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Wir verdienen unsere Hunde nicht — wir werden von ihnen gesegnet.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Hunde bringen Licht in die dunkelsten Ecken unserer Tage.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Jeder Hund hat ein Talent: Er macht sein Zuhause glücklicher.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Ein Hund bereichert das Leben, ohne etwas von dir zu fordern.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Das einzige Problem mit Hunden ist, dass sie zu kurz leben und zu vollständig lieben.", "autor": "Agnes Sligh Turnbull", "kategorie": "liebe" }, - { "text": "Until one has loved an animal, a part of one's soul remains unawakened.", "autor": "Anatole France", "kategorie": "liebe" }, - { "text": "Dogs are how people would be if the important stuff is all that mattered to us.", "autor": "Ashly Lorenzana", "kategorie": "weisheit" }, - { "text": "Ich bin kein Hundenarr. Ich habe einfach einen Hund, der eine eigene Meinung hat, und ich respektiere das.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ein Hund weiß nichts von Montagen — er weiß nur, dass du da bist.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde erfinden sich jeden Tag neu — mit demselben Wedeln.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Ein Hund zu sein bedeutet: vollständig glücklich sein mit dem, was ist.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Wer einmal die Liebe eines Hundes erlebt hat, zweifelt nie mehr an gutem Leben.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Kein Geld der Welt kann die Freude eines Hundes kaufen — nur verdienen.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde machen keine großen Versprechen. Sie halten einfach kleine.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "In meinem Haus regiert der Hund — ich bin nur der Butler.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Wenn mein Hund spricht, spreche ich auch. Unsere Unterhaltungen sind die besten.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Hunde täuschen nicht vor, glücklich zu sein. Wenn sie es zeigen, meinen sie es.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund zeigt dir: Dankbarkeit kann man auch ohne Worte ausdrücken.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Wenn der Hund wedelt, ist der Tag gerettet.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Hunde sind Experten darin, das Beste im Menschen zu sehen.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Eine Pfote auf dem Schoß ist mehr wert als tausend Worte des Trostes.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Wer Hunde nicht mag, hat sie entweder nie richtig kennengelernt oder das Herz zu weit verschlossen.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Ein Hund ist der beste Gesprächspartner: Er hört immer zu und widerspricht nie.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Hunde schnarchen lauter als manche Menschen. Und trotzdem stört es mich nicht.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ein Welpe ist wie ein Sonnenschein, den du im Haus trägst.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Mit einem alten Hund zu leben ist ein Privileg — er hat viel zu erzählen, wenn man zuhört.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Hunde altern mit Würde. Wir sollten es ihnen gleichtun.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein alter Hund hat mehr gelernt als ein junger — aber er braucht mehr Pausen.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Kein Hundeblick ist so tief wie der eines alten Hundes, der sein Leben hinter sich hat.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde wachsen mit uns auf — und lassen uns nie ganz los.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund ist von Geburt bis Tod dein treuer Gefährte — das ist der reinste Pakt der Welt.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Den Charakter eines Menschen erkennt man daran, wie er seinen Hund behandelt.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Wer gut zu Hunden ist, ist gut zu Menschen — das Gegenteil gilt leider auch.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund erzählt dir nicht, was er fühlt — er zeigt es dir.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Respekt und Vertrauen — das sind die Grundlagen jeder Mensch-Hund-Beziehung.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Hundeerziehung beginnt am ersten Tag und endet nie — sie wird nur besser.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Ein Hund der niemals Nein sagt, hat nie gelernt Grenzen zu respektieren.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Hunde lieben Routinen — sie machen das Unbekannte beherrschbar.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Ein Hund, dem man Vertrauen schenkt, enttäuscht selten.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Hunde brauchen keine langen Befehle — ein Wort und ein klares Signal genügen.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Ein glücklicher Hund hat genug Bewegung, genug Aufgaben und genug Liebe.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Wer mit seinem Hund Tricks übt, übt gleichzeitig Geduld und Ausdauer.", "autor": "Unbekannt", "kategorie": "training" }, - { "text": "Ich sage nicht, dass mein Hund klüger ist als du — aber er hat das schon gedacht.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Wenn mein Hund Telefonkonferenzen hätte, würden sie pünktlich enden.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ein Hund ist der einzige, der glücklich ist, wenn du wieder von der Toilette kommst.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ich bin nicht süchtig nach meinem Hund. Ich brauche ihn nur täglich.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Mein Hund lässt mich nie ausreden — das mag ich an ihm.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Sie sagten, ein Hund sei viel Arbeit. Sie hatten recht. Die Freude ist noch mehr.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Hunde machen das Leben besser — das ist keine Meinung, das ist Tatsache.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Kein Selfie gelingt so gut wie das mit dem Hund.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Wer einen Hund hat, braucht keinen Wecker — und kein Sofa für sich allein.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Hunde sind die perfekten Mitbewohner: still, wenn gewünscht, laut, wenn nötig.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Ein Hund ist das einzige Wesen, das dich liebt, obwohl es weiß, dass du seine Spielzeuge wegschmeißt.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Hunde riechen die Lüge — Menschen riechen Parfum.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund braucht dich nicht zu verstehen — er muss dich nur fühlen.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde wissen, wann du weinst. Und sie kommen trotzdem — oder gerade dann.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Ein Hund ist das beste Argument gegen Einsamkeit.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Wer Hunde versteht, versteht ein Stück mehr von der Welt.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Hunde haben keine Maske. Das ist ihr größtes Geschenk an uns.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund fragt nicht nach deiner Geschichte — er schreibt mit dir die nächsten Seiten.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Hunde machen keine Witze auf Kosten anderer. Lehre von ihnen.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund zu haben bedeutet: täglich Dankbarkeit erfahren.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Niemand begrüßt dich aufrichtiger als dein Hund.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund gibt dir immer eine zweite Chance — und eine dritte und vierte.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde machen Fehler wie alle — aber sie entschuldigen sich täglich mit ihrem Blick.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund mit gebrochenem Bein hat mehr Stolz als mancher Mensch.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Der mutigste Beschützer wiegt manchmal nur fünf Kilo.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Hunde fürchten sich manchmal — aber sie überwinden es für dich.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Ein Hund kämpft nie für Macht — nur für seine Menschen.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Hunde haben keine Karriere, kein Konto, keine Ambitionen — und sind glücklicher als die meisten.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Das Geheimnis eines langen Lebens: jeden Tag Gassi gehen.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Frische Luft, Bewegung, ein Hund an der Seite — besser geht Therapie nicht.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Wer regelmäßig mit dem Hund läuft, braucht keinen Motivationscoach.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Hunde halten uns davon ab, den ganzen Tag zu sitzen — das ist ihre heimliche Mission.", "autor": "Unbekannt", "kategorie": "gesundheit" }, - { "text": "Ein Hundekiss mag nicht hygienisch sein — aber er heilt trotzdem.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Wer mit Hunden lebt, hat ein wärmeres Zuhause — buchstäblich.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Hunde zeigen uns täglich: Das Schönste am Tag ist der Anfang und das Ende — weil du da bist.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund braucht weder Smartphone noch Social Media — er lebt das echte Leben.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Wenn ich meinen Hund anschaue, verstehe ich, was Unschuld bedeutet.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund sieht in dir nie das Schlechteste — nur das Beste.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde sind die ehrlichsten Kritiker: Wenn sie fressen, schmeckt es.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Mein Hund gibt mir täglich einen Grund zu lächeln — ohne es zu wissen.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Alles, was du brauchst, ist Liebe — und ein Hund, der das auch so sieht.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Ein Hund macht das Schweigen gemeinsam — das ist echter Frieden.", "autor": "Unbekannt", "kategorie": "liebe" }, - { "text": "Hunde haben das perfekte Leben: schlafen, fressen, spielen, lieben. Kein Stress.", "autor": "Unbekannt", "kategorie": "humor" }, - { "text": "Wer einmal einem Hund beim Schlafen zugesehen hat, weiß, was echte Entspannung ist.", "autor": "Unbekannt", "kategorie": "weisheit" }, - { "text": "Ein Hund zu retten bedeutet, zwei Leben zu retten — seins und deins.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Kaufe nicht, wenn du adoptieren kannst — du gewinnst mehr, als du weißt.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Ein Tierheim-Hund weiß, was Verlassenheit ist — und liebt dich trotzdem, sobald du da bist.", "autor": "Unbekannt", "kategorie": "treue" }, - { "text": "Jeder Hund verdient ein Zuhause. Jeder Mensch verdient einen Hund.", "autor": "Unbekannt", "kategorie": "allgemein" }, - { "text": "Die beste Zeit, einen Hund zu adoptieren, war gestern. Die zweitbeste ist heute.", "autor": "Unbekannt", "kategorie": "allgemein" } -] diff --git a/scripts/import_quotes.py b/scripts/import_quotes.py deleted file mode 100644 index 91c38bc..0000000 --- a/scripts/import_quotes.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -"""Importiert dog_quotes.json in die daily_quotes-Tabelle.""" -import json, sqlite3, sys -from pathlib import Path - -DB_PATH = Path(__file__).parent.parent / 'backend' / 'banyaro.db' -JSON_PATH = Path(__file__).parent / 'dog_quotes.json' - -quotes = json.loads(JSON_PATH.read_text()) -conn = sqlite3.connect(DB_PATH) - -existing = conn.execute("SELECT COUNT(*) FROM daily_quotes").fetchone()[0] -if existing > 0: - print(f"{existing} Einträge bereits vorhanden — überspringe Import.") - print("Zum Neuimport: DELETE FROM daily_quotes; zuerst ausführen.") - sys.exit(0) - -conn.executemany( - "INSERT INTO daily_quotes (text, autor, kategorie) VALUES (?, ?, ?)", - [(q['text'], q.get('autor'), q.get('kategorie')) for q in quotes] -) -conn.commit() -print(f"{len(quotes)} Tagessprüche importiert.") -conn.close()