diff --git a/backend/database.py b/backend/database.py
index ab82594..01dfc17 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -189,13 +189,6 @@ def init_db():
);
CREATE INDEX IF NOT EXISTS idx_route_walks_user ON route_walks(user_id);
- CREATE TABLE IF NOT EXISTS route_dogs (
- route_id INTEGER NOT NULL REFERENCES routes(id) ON DELETE CASCADE,
- dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
- PRIMARY KEY (route_id, dog_id)
- );
- CREATE INDEX IF NOT EXISTS idx_route_dogs_dog ON route_dogs(dog_id);
-
CREATE TABLE IF NOT EXISTS exercise_progress (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@@ -1981,38 +1974,6 @@ def _migrate(conn_factory):
""")
logger.info("Migration: futter_profil bereit.")
- # Futter-Einträge & Reaktionen (Verträglichkeits-Tracking)
- try:
- conn.executescript("""
- CREATE TABLE IF NOT EXISTS futter_eintraege (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
- datum TEXT NOT NULL,
- uhrzeit TEXT NOT NULL,
- futter_name TEXT NOT NULL,
- futter_typ TEXT NOT NULL DEFAULT 'trockenfutter',
- menge_g INTEGER,
- notiz TEXT,
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
- );
- CREATE INDEX IF NOT EXISTS idx_futter_eintraege_dog ON futter_eintraege(dog_id, datum DESC);
-
- CREATE TABLE IF NOT EXISTS futter_reaktionen (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
- datum TEXT NOT NULL,
- uhrzeit TEXT NOT NULL,
- reaktion_typ TEXT NOT NULL,
- intensitaet INTEGER NOT NULL DEFAULT 3,
- notiz TEXT,
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
- );
- CREATE INDEX IF NOT EXISTS idx_futter_reaktionen_dog ON futter_reaktionen(dog_id, datum DESC);
- """)
- logger.info("Migration: futter_eintraege + futter_reaktionen bereit.")
- except Exception as e:
- logger.warning(f"Migration futter_eintraege/reaktionen: {e}")
-
# Wiederkehrende Ausgaben (Daueraufträge)
conn.executescript("""
CREATE TABLE IF NOT EXISTS recurring_expenses (
@@ -2143,85 +2104,6 @@ def _migrate(conn_factory):
except Exception:
pass # Spalte existiert bereits
- # exercise_progress + training_plan_progress: dog_id ergänzen
- existing_ep = [r[1] for r in conn.execute("PRAGMA table_info(exercise_progress)").fetchall()]
- if 'dog_id' not in existing_ep:
- try:
- # Neue Tabelle mit dog_id erstellen
- conn.execute("""
- CREATE TABLE exercise_progress_new (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- dog_id INTEGER REFERENCES dogs(id) ON DELETE CASCADE,
- exercise_id TEXT NOT NULL,
- status TEXT,
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
- UNIQUE(dog_id, exercise_id)
- )
- """)
- # Bestehende Daten migrieren: dog_id = erster Hund des Users
- conn.execute("""
- INSERT INTO exercise_progress_new (user_id, dog_id, exercise_id, status, updated_at)
- SELECT ep.user_id,
- (SELECT id FROM dogs WHERE user_id=ep.user_id ORDER BY id LIMIT 1),
- ep.exercise_id, ep.status, ep.updated_at
- FROM exercise_progress ep
- """)
- conn.execute("DROP TABLE exercise_progress")
- conn.execute("ALTER TABLE exercise_progress_new RENAME TO exercise_progress")
- conn.execute("CREATE INDEX IF NOT EXISTS idx_exercise_progress_user ON exercise_progress(user_id)")
- conn.execute("CREATE INDEX IF NOT EXISTS idx_exercise_progress_dog ON exercise_progress(dog_id)")
- logger.info("Migration: exercise_progress.dog_id hinzugefügt.")
- except Exception as e:
- logger.warning(f"Migration exercise_progress.dog_id fehlgeschlagen: {e}")
-
- existing_tp = [r[1] for r in conn.execute("PRAGMA table_info(training_plan_progress)").fetchall()]
- if 'dog_id' not in existing_tp:
- try:
- conn.execute("""
- CREATE TABLE training_plan_progress_new (
- user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- dog_id INTEGER REFERENCES dogs(id) ON DELETE CASCADE,
- item_key TEXT NOT NULL,
- checked INTEGER NOT NULL DEFAULT 1,
- checked_at TEXT NOT NULL DEFAULT (datetime('now')),
- PRIMARY KEY (dog_id, item_key)
- )
- """)
- conn.execute("""
- INSERT INTO training_plan_progress_new (user_id, dog_id, item_key, checked, checked_at)
- SELECT tp.user_id,
- (SELECT id FROM dogs WHERE user_id=tp.user_id ORDER BY id LIMIT 1),
- tp.item_key, tp.checked, tp.checked_at
- FROM training_plan_progress tp
- """)
- conn.execute("DROP TABLE training_plan_progress")
- conn.execute("ALTER TABLE training_plan_progress_new RENAME TO training_plan_progress")
- logger.info("Migration: training_plan_progress.dog_id hinzugefügt.")
- except Exception as e:
- logger.warning(f"Migration training_plan_progress.dog_id fehlgeschlagen: {e}")
-
- # verstorben_am: Hund als verstorben markierbar
- try:
- conn.execute("ALTER TABLE dogs ADD COLUMN verstorben_am TEXT")
- logger.info("Migration: dogs.verstorben_am hinzugefügt.")
- except Exception:
- pass
-
- # route_dogs: bestehende Routen allen Hunden des Users zuweisen
- try:
- existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0]
- if existing == 0:
- conn.execute("""
- INSERT OR IGNORE INTO route_dogs (route_id, dog_id)
- SELECT r.id, d.id
- FROM routes r
- JOIN dogs d ON d.user_id = r.user_id
- """)
- logger.info("Migration: route_dogs mit bestehenden Routen befüllt.")
- except Exception as e:
- logger.warning(f"Migration route_dogs fehlgeschlagen: {e}")
-
def _seed_help_articles(conn):
"""Befüllt help_articles mit Starter-FAQs — nur wenn die Tabelle noch leer ist."""
diff --git a/backend/main.py b/backend/main.py
index dc86828..3ebd1fa 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -179,10 +179,7 @@ class MediaCacheMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
if request.url.path.startswith('/media/'):
- if os.getenv('STAGING') == 'true':
- response.headers['Cache-Control'] = 'no-cache'
- else:
- response.headers['Cache-Control'] = 'public, max-age=31536000, immutable'
+ response.headers['Cache-Control'] = 'public, max-age=31536000, immutable'
return response
app.add_middleware(MediaCacheMiddleware)
@@ -253,7 +250,6 @@ from routes.ernaehrung import router as ernaehrung_router
from routes.challenges import router as challenges_router
from routes.gassi_zeiten import router as gassi_zeiten_router
from routes.help import router as help_router
-from routes.feedback import router as feedback_router
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
@@ -316,7 +312,6 @@ app.include_router(ernaehrung_router, prefix="/api/dogs", tag
app.include_router(challenges_router, prefix="/api/challenges", tags=["Foto-Challenge"])
app.include_router(gassi_zeiten_router, prefix="/api/gassi-zeiten", tags=["Gassi-Zeiten"])
app.include_router(help_router, prefix="/api/help", tags=["Hilfe/FAQ"])
-app.include_router(feedback_router, prefix="/api/feedback", tags=["Feedback"])
# ------------------------------------------------------------------
@@ -344,39 +339,9 @@ app.mount("/img", StaticFiles(directory=f"{STATIC_DIR}/img"), name="img")
# User-generierte Medien (Fotos aus Tagebuch, Giftköder-Alarm, etc.)
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
os.makedirs(MEDIA_DIR, exist_ok=True)
+app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
-STAGING = os.getenv("STAGING", "false").lower() == "true"
-PROD_MEDIA_DIR = "/prod-media"
-
-_MIME_MAP = {
- ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png",
- ".webp": "image/webp", ".gif": "image/gif", ".mp4": "video/mp4",
- ".webm": "video/webm", ".pdf": "application/pdf",
-}
-
-if STAGING and os.path.isdir(PROD_MEDIA_DIR):
- # Staging: eigene Uploads in MEDIA_DIR, Fallback auf Prod-Medien (read-only)
- from fastapi.responses import FileResponse as _FileResponse
-
- def _media_response(filepath: str):
- ext = os.path.splitext(filepath)[1].lower()
- mt = _MIME_MAP.get(ext, "application/octet-stream")
- return _FileResponse(filepath, media_type=mt)
-
- @app.api_route("/media/{path:path}", methods=["GET", "HEAD"])
- async def serve_media_staging(path: str):
- staging_file = os.path.join(MEDIA_DIR, path)
- if os.path.isfile(staging_file):
- return _media_response(staging_file)
- prod_file = os.path.join(PROD_MEDIA_DIR, path)
- if os.path.isfile(prod_file):
- return _media_response(prod_file)
- from fastapi import HTTPException as _HE
- raise _HE(404, "Media not found")
-else:
- app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
-
-APP_VER = "872" # muss mit APP_VER in app.js übereinstimmen
+APP_VER = "826" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json")
async def assetlinks():
diff --git a/backend/routes/admin.py b/backend/routes/admin.py
index 92a199d..5e82927 100644
--- a/backend/routes/admin.py
+++ b/backend/routes/admin.py
@@ -583,48 +583,6 @@ async def scheduler_trigger(job_id: str, user=Depends(require_admin)):
return {"ok": True, "job_id": job_id}
-# ------------------------------------------------------------------
-# GET /api/admin/referrals — User-wirbt-User Top 100
-# ------------------------------------------------------------------
-@router.get("/referrals")
-async def referral_stats(user=Depends(require_mod)):
- with db() as conn:
- # Top-Werber mit Anzahl
- top = conn.execute("""
- SELECT r.id, r.name, r.email,
- COUNT(u.id) AS invited_count,
- r.created_at AS member_since
- FROM users u
- JOIN users r ON r.id = u.referred_by
- GROUP BY r.id
- ORDER BY invited_count DESC
- LIMIT 100
- """).fetchall()
-
- # Alle Einladungen (für Detail-Ansicht)
- invites = conn.execute("""
- SELECT u.id, u.name, u.email, u.created_at,
- r.id AS referrer_id, r.name AS referrer_name
- FROM users u
- JOIN users r ON r.id = u.referred_by
- ORDER BY u.created_at DESC
- LIMIT 500
- """).fetchall()
-
- total_users = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
- total_referred = conn.execute(
- "SELECT COUNT(*) FROM users WHERE referred_by IS NOT NULL"
- ).fetchone()[0]
-
- return {
- "top_referrers": [dict(r) for r in top],
- "recent_invites": [dict(r) for r in invites],
- "total_users": total_users,
- "total_referred": total_referred,
- "viral_factor": round(total_referred / max(total_users - total_referred, 1), 2),
- }
-
-
# ------------------------------------------------------------------
# GET /api/admin/ki/history — 30-Tage-Verlauf + Top-User (all-time)
# ------------------------------------------------------------------
diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py
index e2985dd..6c35334 100644
--- a/backend/routes/dogs.py
+++ b/backend/routes/dogs.py
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import Optional
from database import db
-from auth import get_current_user, has_pro_access
+from auth import get_current_user
from routes.push import send_push_to_user
from media_utils import safe_media_path, preview_url_from
@@ -41,7 +41,7 @@ class DogUpdate(BaseModel):
async def list_dogs(user=Depends(get_current_user)):
with db() as conn:
own = conn.execute(
- "SELECT *, NULL AS shared_by, NULL AS share_role FROM dogs WHERE user_id=? AND (verstorben_am IS NULL) ORDER BY id",
+ "SELECT *, NULL AS shared_by, NULL AS share_role FROM dogs WHERE user_id=? ORDER BY id",
(user["id"],)
).fetchall()
shared = conn.execute(
@@ -131,14 +131,6 @@ def _is_plausible_dog(name: str, rasse: str, geburtstag) -> tuple[bool, str]:
@router.post("")
async def create_dog(data: DogCreate, user=Depends(get_current_user)):
with db() as conn:
- existing = conn.execute(
- "SELECT COUNT(*) FROM dogs WHERE user_id=?", (user["id"],)
- ).fetchone()[0]
- if existing >= 1 and not has_pro_access(user):
- raise HTTPException(
- status_code=403,
- detail="Mehrere Hunde sind ein Pro-Feature. Upgrade auf Ban Yaro Pro, um weitere Hunde anzulegen."
- )
conn.execute(
"""INSERT INTO dogs (user_id, name, rasse, geburtstag, geschlecht,
gewicht_kg, chip_nr, bio, is_public)
@@ -188,7 +180,8 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
if not dog:
raise HTTPException(404, "Hund nicht gefunden.")
- # Hintergrundfoto: Querformat-Bilder bevorzugt, tagesweise rotierend
+ # 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
@@ -197,13 +190,12 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
ORDER BY d.datum DESC, d.id DESC, dm.id ASC""",
(dog_id,)
).fetchall()
- # Fallback: Bilder ohne Dimensionsdaten (vor dem Backfill hochgeladen)
+ # 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'
- AND dm.img_width IS NULL
ORDER BY d.datum DESC, d.id DESC, dm.id ASC""",
(dog_id,)
).fetchall()
@@ -255,16 +247,15 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
day_num = (_dt.date.today() - _dt.date(2024, 1, 1)).days
# Versuche JOIN (funktioniert wenn js_exercise_id-Spalte vorhanden)
- # Nur Übungen des aktiven Hundes, 'sitzt' ausschließen
try:
joined = conn.execute(
"""SELECT ep.exercise_id, te.name, te.kategorie AS kategorie_raw,
te.schwierigkeit, te.js_exercise_id
FROM exercise_progress ep
JOIN training_exercises te ON te.js_exercise_id = ep.exercise_id
- WHERE ep.dog_id = ? AND ep.status IN ('noch-nicht', 'manchmal', 'meistens')
+ WHERE ep.user_id = ? AND ep.status IN ('noch-nicht', 'manchmal', 'meistens')
ORDER BY ep.updated_at ASC LIMIT 50""",
- (dog_id,)
+ (user["id"],)
).fetchall()
except Exception:
joined = []
@@ -289,9 +280,9 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
)
raw = conn.execute(
"""SELECT exercise_id FROM exercise_progress
- WHERE dog_id = ? AND status IN ('noch-nicht', 'manchmal', 'meistens')
+ WHERE user_id = ? AND status IN ('noch-nicht', 'manchmal', 'meistens')
ORDER BY updated_at ASC LIMIT 50""",
- (dog_id,)
+ (user["id"],)
).fetchall()
valid = [r["exercise_id"] for r in raw
if any(r["exercise_id"].startswith(p) for p in _KNOWN_PREFIXES)]
@@ -780,21 +771,6 @@ async def get_hunde_buch(
return HTMLResponse(content=html_page)
-# ------------------------------------------------------------------
-# GET /api/dogs/verstorben — Alle verstorbenen Hunde des Users
-# ------------------------------------------------------------------
-@router.get("/verstorben")
-async def get_verstorbene_hunde(user=Depends(get_current_user)):
- with db() as conn:
- rows = conn.execute(
- """SELECT id, name, rasse, foto_url, verstorben_am, geburtstag
- FROM dogs WHERE user_id=? AND verstorben_am IS NOT NULL
- ORDER BY verstorben_am DESC""",
- (user["id"],)
- ).fetchall()
- return [dict(r) for r in rows]
-
-
@router.get("/{dog_id}")
async def get_dog(dog_id: int, user=Depends(get_current_user)):
with db() as conn:
@@ -1224,15 +1200,14 @@ async def get_dog_timeline(dog_id: int, user=Depends(get_current_user)):
"ref_id": r["id"],
})
- # --- Routen (nur Routen wo dieser Hund mitgegangen ist) ---
+ # --- Routen ---
route_rows = conn.execute(
- """SELECT r.id, r.name, r.distanz_km,
- date(r.created_at) AS datum
- FROM routes r
- JOIN route_dogs rd ON rd.route_id = r.id AND rd.dog_id = ?
- WHERE r.user_id = ?
- ORDER BY r.created_at ASC""",
- (dog_id, user["id"])
+ """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
@@ -1280,108 +1255,3 @@ async def get_dog_timeline(dog_id: int, user=Depends(get_current_user)):
"geburtstag": dog["geburtstag"],
"events": events,
}
-
-
-# ------------------------------------------------------------------
-# POST /api/dogs/{id}/gedenken — Hund als verstorben markieren
-# ------------------------------------------------------------------
-class GedenkenData(BaseModel):
- verstorben_am: str # YYYY-MM-DD
-
-@router.post("/{dog_id}/gedenken")
-async def mark_verstorben(dog_id: int, data: GedenkenData, user=Depends(get_current_user)):
- with db() as conn:
- updated = conn.execute(
- "UPDATE dogs SET verstorben_am=? WHERE id=? AND user_id=?",
- (data.verstorben_am, dog_id, user["id"])
- ).rowcount
- if not updated:
- raise HTTPException(404, "Hund nicht gefunden.")
- return {"ok": True}
-
-
-# ------------------------------------------------------------------
-# GET /api/dogs/{id}/gedenkseite — Memorial-Daten
-# ------------------------------------------------------------------
-@router.get("/{dog_id}/gedenkseite")
-async def get_gedenkseite(dog_id: int, user=Depends(get_current_user)):
- with db() as conn:
- dog = conn.execute(
- "SELECT * FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"])
- ).fetchone()
- if not dog:
- raise HTTPException(404)
- dog = dict(dog)
-
- # Statistiken
- km_total = conn.execute(
- "SELECT COALESCE(ROUND(SUM(distanz_km),1),0) AS km FROM routes r "
- "JOIN route_dogs rd ON rd.route_id=r.id WHERE rd.dog_id=?", (dog_id,)
- ).fetchone()["km"]
-
- diary_count = conn.execute(
- "SELECT COUNT(*) FROM diary WHERE dog_id=?", (dog_id,)
- ).fetchone()[0]
-
- media_count = conn.execute(
- "SELECT COUNT(*) FROM diary_media dm JOIN diary d ON d.id=dm.diary_id "
- "WHERE d.dog_id=? AND dm.media_type='image'", (dog_id,)
- ).fetchone()[0]
-
- training_count = conn.execute(
- "SELECT COUNT(*) FROM training_sessions WHERE dog_id=?", (dog_id,)
- ).fetchone()[0]
-
- # Letzter Tagebucheintrag
- last_entry = conn.execute(
- "SELECT titel, datum FROM diary WHERE dog_id=? ORDER BY datum DESC LIMIT 1",
- (dog_id,)
- ).fetchone()
-
- # Erste und letzte Aufnahme
- first_entry = conn.execute(
- "SELECT datum FROM diary WHERE dog_id=? ORDER BY datum ASC LIMIT 1",
- (dog_id,)
- ).fetchone()
-
- # Letzte 6 Fotos für Galerie
- photos = conn.execute(
- """SELECT dm.url FROM diary_media dm
- JOIN diary d ON d.id=dm.diary_id
- WHERE d.dog_id=? AND dm.media_type='image'
- AND dm.img_width IS NOT NULL AND dm.img_width > dm.img_height
- ORDER BY d.datum DESC, dm.id DESC LIMIT 6""",
- (dog_id,)
- ).fetchall()
- if not photos:
- photos = conn.execute(
- """SELECT dm.url FROM diary_media dm
- JOIN diary d ON d.id=dm.diary_id
- WHERE d.dog_id=? AND dm.media_type='image'
- ORDER BY d.datum DESC, dm.id DESC LIMIT 6""",
- (dog_id,)
- ).fetchall()
-
- # Gemeinsame Zeit berechnen
- joined = dog.get("geburtstag") or (first_entry["datum"] if first_entry else None)
- passed = dog.get("verstorben_am")
- gemeinsam_tage = None
- if joined and passed:
- try:
- from datetime import date as _date
- d1 = _date.fromisoformat(joined)
- d2 = _date.fromisoformat(passed)
- gemeinsam_tage = (d2 - d1).days
- except Exception:
- pass
-
- return {
- "dog": dog,
- "km_total": km_total,
- "diary_count": diary_count,
- "media_count": media_count,
- "training_count": training_count,
- "last_entry": dict(last_entry) if last_entry else None,
- "gemeinsam_tage": gemeinsam_tage,
- "photos": [r["url"] for r in photos],
- }
diff --git a/backend/routes/ernaehrung.py b/backend/routes/ernaehrung.py
index 2aa4760..c1f850e 100644
--- a/backend/routes/ernaehrung.py
+++ b/backend/routes/ernaehrung.py
@@ -143,305 +143,3 @@ async def ki_ernaehrung(dog_id: int, body: KiBeratungRequest,
raise HTTPException(503, str(e))
except Exception:
raise HTTPException(500, "KI momentan nicht verfügbar.")
-
-
-# ==================================================================
-# FUTTER-VERTRÄGLICHKEIT
-# ==================================================================
-
-REAKTION_TYPEN = {
- # Positiv
- "verdauung_gut": {"label": "Gute Verdauung", "kategorie": "positiv", "fenster_h": 8},
- "energie_hoch": {"label": "Viel Energie", "kategorie": "positiv", "fenster_h": 12},
- "fell_glaenzend": {"label": "Glänzendes Fell", "kategorie": "positiv", "fenster_h": 336}, # 2 Wochen
- # Gastrointestinal
- "erbrechen": {"label": "Erbrechen", "kategorie": "gastro_negativ", "fenster_h": 6},
- "durchfall": {"label": "Durchfall", "kategorie": "gastro_negativ", "fenster_h": 8},
- "blaehungen": {"label": "Blähungen", "kategorie": "gastro_negativ", "fenster_h": 6},
- "weicher_stuhl": {"label": "Weicher Stuhl", "kategorie": "gastro_negativ", "fenster_h": 8},
- "appetitlosigkeit":{"label": "Appetitlosigkeit", "kategorie": "gastro_negativ", "fenster_h": 12},
- # Haut & Fell
- "juckreiz": {"label": "Juckreiz / Kratzen", "kategorie": "haut_negativ", "fenster_h": 72},
- "haarausfall": {"label": "Haarausfall", "kategorie": "haut_negativ", "fenster_h": 336},
- "stumpfes_fell": {"label": "Stumpfes Fell", "kategorie": "haut_negativ", "fenster_h": 336},
- "schuppenbildung":{"label": "Schuppenbildung", "kategorie": "haut_negativ", "fenster_h": 168},
- "roetungen": {"label": "Hautrötungen / Entzündung", "kategorie": "haut_negativ", "fenster_h": 72},
- "pfotenlecken": {"label": "Pfoten lecken (chronisch)", "kategorie": "haut_negativ", "fenster_h": 168},
- "ohrentzuendung": {"label": "Ohrentzündung", "kategorie": "haut_negativ", "fenster_h": 168},
- "fettiges_fell": {"label": "Fettiges Fell / Seborrhö", "kategorie": "haut_negativ", "fenster_h": 336},
- # Allgemeinbefinden
- "schlappheit": {"label": "Schlappheit / Apathie", "kategorie": "allgemein_negativ", "fenster_h": 12},
- "nervositaet": {"label": "Nervosität / Unruhe", "kategorie": "allgemein_negativ", "fenster_h": 12},
- "viel_trinken": {"label": "Ungewöhnlich viel trinken", "kategorie": "allgemein_negativ", "fenster_h": 24},
- "sonstiges": {"label": "Sonstiges", "kategorie": "sonstiges", "fenster_h": 24},
-}
-
-_POSITIV_KAT = {"positiv"}
-_NEGATIV_KAT = {"gastro_negativ", "haut_negativ", "allgemein_negativ"}
-_HAUT_HINWEIS = "Haut- & Fell-Symptome wie {label} entwickeln sich typischerweise über Wochen. Mindestens 4–6 Wochen Beobachtung empfohlen — auch nach einem Futterwechsel dauert eine Besserung 2–6 Wochen."
-_GASTRO_HINWEIS = "Magen-Darm-Symptome wie {label} treten meist innerhalb weniger Stunden auf. Wenn sie häufig wiederkehren, ist ein Tierarztbesuch empfohlen."
-
-
-class FutterEintragCreate(BaseModel):
- datum: str
- uhrzeit: str
- futter_name: str
- futter_typ: Optional[str] = "trockenfutter"
- menge_g: Optional[int] = None
- notiz: Optional[str] = None
-
-
-class ReaktionCreate(BaseModel):
- datum: str
- uhrzeit: str
- reaktion_typ: str
- intensitaet: Optional[int] = 3
- notiz: Optional[str] = None
-
-
-# ------------------------------------------------------------------
-# POST /dogs/{dog_id}/futter
-# ------------------------------------------------------------------
-@router.post("/{dog_id}/futter")
-async def create_futter_eintrag(dog_id: int, body: FutterEintragCreate,
- user=Depends(get_current_user)):
- with db() as conn:
- _check_dog_access(conn, dog_id, user["id"])
- cur = conn.execute("""
- INSERT INTO futter_eintraege
- (dog_id, datum, uhrzeit, futter_name, futter_typ, menge_g, notiz)
- VALUES (?, ?, ?, ?, ?, ?, ?)
- """, (dog_id, body.datum, body.uhrzeit, body.futter_name,
- body.futter_typ or "trockenfutter", body.menge_g, body.notiz))
- row = conn.execute(
- "SELECT * FROM futter_eintraege WHERE id=?", (cur.lastrowid,)
- ).fetchone()
- return dict(row)
-
-
-# ------------------------------------------------------------------
-# GET /dogs/{dog_id}/futter
-# ------------------------------------------------------------------
-@router.get("/{dog_id}/futter")
-async def list_futter_eintraege(dog_id: int, user=Depends(get_current_user)):
- with db() as conn:
- _check_dog_access(conn, dog_id, user["id"])
- rows = conn.execute("""
- SELECT * FROM futter_eintraege
- WHERE dog_id=?
- ORDER BY datum DESC, uhrzeit DESC
- LIMIT 50
- """, (dog_id,)).fetchall()
- return [dict(r) for r in rows]
-
-
-# ------------------------------------------------------------------
-# DELETE /dogs/{dog_id}/futter/{entry_id}
-# ------------------------------------------------------------------
-@router.delete("/{dog_id}/futter/{entry_id}")
-async def delete_futter_eintrag(dog_id: int, entry_id: int,
- user=Depends(get_current_user)):
- with db() as conn:
- _check_dog_access(conn, dog_id, user["id"])
- result = conn.execute(
- "DELETE FROM futter_eintraege WHERE id=? AND dog_id=?",
- (entry_id, dog_id)
- )
- if result.rowcount == 0:
- raise HTTPException(404, "Eintrag nicht gefunden.")
- return {"ok": True}
-
-
-# ------------------------------------------------------------------
-# POST /dogs/{dog_id}/futter/reaktion
-# ------------------------------------------------------------------
-@router.post("/{dog_id}/futter/reaktion")
-async def create_reaktion(dog_id: int, body: ReaktionCreate,
- user=Depends(get_current_user)):
- with db() as conn:
- _check_dog_access(conn, dog_id, user["id"])
- cur = conn.execute("""
- INSERT INTO futter_reaktionen
- (dog_id, datum, uhrzeit, reaktion_typ, intensitaet, notiz)
- VALUES (?, ?, ?, ?, ?, ?)
- """, (dog_id, body.datum, body.uhrzeit, body.reaktion_typ,
- body.intensitaet or 3, body.notiz))
- row = conn.execute(
- "SELECT * FROM futter_reaktionen WHERE id=?", (cur.lastrowid,)
- ).fetchone()
- return dict(row)
-
-
-# ------------------------------------------------------------------
-# GET /dogs/{dog_id}/futter/reaktionen
-# ------------------------------------------------------------------
-@router.get("/{dog_id}/futter/reaktionen")
-async def list_reaktionen(dog_id: int, user=Depends(get_current_user)):
- with db() as conn:
- _check_dog_access(conn, dog_id, user["id"])
- rows = conn.execute("""
- SELECT * FROM futter_reaktionen
- WHERE dog_id=?
- ORDER BY datum DESC, uhrzeit DESC
- LIMIT 50
- """, (dog_id,)).fetchall()
- return [dict(r) for r in rows]
-
-
-# ------------------------------------------------------------------
-# DELETE /dogs/{dog_id}/futter/reaktion/{react_id}
-# ------------------------------------------------------------------
-@router.delete("/{dog_id}/futter/reaktion/{react_id}")
-async def delete_reaktion(dog_id: int, react_id: int,
- user=Depends(get_current_user)):
- with db() as conn:
- _check_dog_access(conn, dog_id, user["id"])
- result = conn.execute(
- "DELETE FROM futter_reaktionen WHERE id=? AND dog_id=?",
- (react_id, dog_id)
- )
- if result.rowcount == 0:
- raise HTTPException(404, "Reaktion nicht gefunden.")
- return {"ok": True}
-
-
-# ------------------------------------------------------------------
-# GET /dogs/{dog_id}/futter/analyse
-# ------------------------------------------------------------------
-@router.get("/{dog_id}/futter/analyse")
-async def futter_analyse(dog_id: int, user=Depends(get_current_user)):
- from datetime import datetime
-
- with db() as conn:
- _check_dog_access(conn, dog_id, user["id"])
-
- eintraege = conn.execute(
- "SELECT * FROM futter_eintraege WHERE dog_id=? ORDER BY datum, uhrzeit",
- (dog_id,)
- ).fetchall()
- reaktionen = conn.execute(
- "SELECT * FROM futter_reaktionen WHERE dog_id=? ORDER BY datum, uhrzeit",
- (dog_id,)
- ).fetchall()
-
- def parse_ts(datum, uhrzeit):
- try:
- return datetime.fromisoformat(f"{datum}T{uhrzeit}")
- except Exception:
- return None
-
- # futter_name → {typ, mahlzeiten, positiv, negativ, kategorien: {kat: count}}
- futter_stats: dict = {}
-
- for e in eintraege:
- name = e["futter_name"]
- if name not in futter_stats:
- futter_stats[name] = {
- "name": name,
- "typ": e["futter_typ"],
- "mahlzeiten": 0,
- "positiv": 0,
- "negativ": 0,
- "kategorien": {},
- }
- futter_stats[name]["mahlzeiten"] += 1
-
- for r in reaktionen:
- r_ts = parse_ts(r["datum"], r["uhrzeit"])
- if not r_ts:
- continue
- r_typ = r["reaktion_typ"]
- meta = REAKTION_TYPEN.get(r_typ, {"kategorie": "sonstiges", "fenster_h": 24})
- kat = meta["kategorie"]
- fenster = meta["fenster_h"]
- # Mindestfenster 1h, maximales Fenster wie angegeben
- min_h = 1
-
- for e in eintraege:
- e_ts = parse_ts(e["datum"], e["uhrzeit"])
- if not e_ts:
- continue
- diff = (r_ts - e_ts).total_seconds() / 3600
- if min_h <= diff <= fenster:
- name = e["futter_name"]
- if name not in futter_stats:
- continue
- if kat in _POSITIV_KAT:
- futter_stats[name]["positiv"] += 1
- elif kat in _NEGATIV_KAT:
- futter_stats[name]["negativ"] += 1
- # Kategorie-Zähler
- futter_stats[name]["kategorien"][kat] = \
- futter_stats[name]["kategorien"].get(kat, 0) + 1
-
- result_futter = []
- for stats in futter_stats.values():
- positiv = stats["positiv"]
- negativ = stats["negativ"]
- total = positiv + negativ
- if total == 0:
- score = 50
- status = "neu"
- else:
- raw = (positiv - negativ * 2) / max(1, total)
- # raw liegt zwischen -2 und 1 → normieren auf 0-100
- score = int(max(0, min(100, (raw + 2) / 3 * 100)))
- if score >= 60:
- status = "gut"
- elif score >= 30:
- status = "neutral"
- else:
- status = "problematisch"
-
- result_futter.append({
- "name": stats["name"],
- "typ": stats["typ"],
- "mahlzeiten": stats["mahlzeiten"],
- "positiv": positiv,
- "negativ": negativ,
- "score": score,
- "status": status,
- "kategorien": stats["kategorien"],
- })
-
- # Sortierung: problematisch → neutral → gut → neu, dann nach Score
- ORDER = {"problematisch": 0, "neutral": 1, "gut": 2, "neu": 3}
- result_futter.sort(key=lambda x: (ORDER.get(x["status"], 9), -x["score"]))
-
- # Hinweis ableiten: erstes problematisches Futter mit Haut/Gastro-Symptomen
- hinweis = None
- for f in result_futter:
- if f["status"] != "problematisch":
- continue
- kats = f["kategorien"]
- if kats.get("haut_negativ", 0) > 0:
- # Häufigstes Haut-Symptom finden
- haut_rxn = [
- r["reaktion_typ"] for r in reaktionen
- if REAKTION_TYPEN.get(r["reaktion_typ"], {}).get("kategorie") == "haut_negativ"
- ]
- label = REAKTION_TYPEN.get(haut_rxn[0], {}).get("label", "Haut-Symptome") if haut_rxn else "Haut-Symptome"
- hinweis = _HAUT_HINWEIS.format(label=label)
- break
- if kats.get("gastro_negativ", 0) > 0:
- gastro_rxn = [
- r["reaktion_typ"] for r in reaktionen
- if REAKTION_TYPEN.get(r["reaktion_typ"], {}).get("kategorie") == "gastro_negativ"
- ]
- label = REAKTION_TYPEN.get(gastro_rxn[0], {}).get("label", "Magen-Darm-Symptome") if gastro_rxn else "Magen-Darm-Symptome"
- hinweis = _GASTRO_HINWEIS.format(label=label)
- break
-
- # Kategorien-Übersicht über alle Reaktionen
- kategorien_gesamt: dict = {}
- for r in reaktionen:
- kat = REAKTION_TYPEN.get(r["reaktion_typ"], {}).get("kategorie", "sonstiges")
- kategorien_gesamt[kat] = kategorien_gesamt.get(kat, 0) + 1
-
- return {
- "eintraege_count": len(eintraege),
- "reaktionen_count": len(reaktionen),
- "futter": result_futter,
- "kategorien": kategorien_gesamt,
- "hinweis": hinweis,
- }
diff --git a/backend/routes/feedback.py b/backend/routes/feedback.py
deleted file mode 100644
index a5c4792..0000000
--- a/backend/routes/feedback.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""
-BAN YARO — User-Feedback per E-Mail an support@banyaro.app
-"""
-
-from typing import Annotated, Literal
-from fastapi import APIRouter, Depends
-from pydantic import BaseModel, Field
-
-from auth import get_current_user
-from mailer import send_email, email_html
-
-router = APIRouter()
-
-SUPPORT_MAIL = "support@banyaro.app"
-
-KATEGORIEN = {"bug": "🐛 Bug / Fehler", "idee": "💡 Idee / Wunsch", "lob": "🎉 Lob", "sonstiges": "💬 Sonstiges"}
-
-
-class FeedbackIn(BaseModel):
- kategorie: Literal["bug", "idee", "lob", "sonstiges"]
- text: Annotated[str, Field(min_length=5, max_length=2000)]
-
-
-@router.post("")
-async def submit_feedback(
- payload: FeedbackIn,
- user=Depends(get_current_user),
-):
- kat_label = KATEGORIEN.get(payload.kategorie, payload.kategorie)
- username = user.get("name", "?")
- email = user.get("email", "")
- tier = user.get("subscription_tier", "standard")
-
- subject = f"[Feedback] {kat_label} von @{username}"
-
- body = f"""
-
- Neues Feedback aus der App:
-
-
- | Kategorie |
- {kat_label} |
- | User |
- @{username} ({email}) |
- | Tier |
- {tier} |
-
-
-{payload.text}
-
"""
-
- plain = f"Feedback [{kat_label}] von @{username} ({email})\n\n{payload.text}"
-
- await send_email(SUPPORT_MAIL, subject, email_html(body), plain)
- return {"ok": True}
diff --git a/backend/routes/friends.py b/backend/routes/friends.py
index a813532..7df14e4 100644
--- a/backend/routes/friends.py
+++ b/backend/routes/friends.py
@@ -172,20 +172,6 @@ async def send_request(target_id: int, user=Depends(get_current_user)):
return {"ok": True}
-@router.get("/pending")
-async def pending_requests(user=Depends(get_current_user)):
- """Eingehende Freundschaftsanfragen — kein Pro nötig, für Notification-Accept."""
- with db() as conn:
- rows = conn.execute("""
- SELECT f.id, u.name AS requester_name, u.avatar_url
- FROM friendships f
- JOIN users u ON u.id = f.requester_id
- WHERE f.addressee_id=? AND f.status='pending'
- ORDER BY f.created_at DESC
- """, (user["id"],)).fetchall()
- return [dict(r) for r in rows]
-
-
@router.post("/{friendship_id}/accept")
async def accept_request(friendship_id: int, user=Depends(get_current_user)):
uid = user["id"]
diff --git a/backend/routes/ki.py b/backend/routes/ki.py
index 2b16cbe..6521b90 100644
--- a/backend/routes/ki.py
+++ b/backend/routes/ki.py
@@ -55,7 +55,7 @@ Schreibe klar und strukturiert, ohne unnötigen Fachjargon."""
prompt=prompt,
system=system,
max_tokens=600,
- requires_premium=True,
+ requires_premium=False,
user_id=user["id"],
)
return {"antwort": result}
@@ -361,65 +361,3 @@ Falls kein Hund erkennbar: ist_hund=false und leeres rassen-Array."""
"hinweis": parsed.get("hinweis") or None,
"verbleibende_anfragen": remaining_after,
}
-
-
-# ------------------------------------------------------------------
-# POST /ki/abschied — Persönlicher Abschiedstext für verstorbenen Hund
-# ------------------------------------------------------------------
-class AbschiedRequest(BaseModel):
- dog_id: int
- name: str
- rasse: Optional[str] = None
- km_total: Optional[float] = None
- diary_count: Optional[int] = None
- gemeinsam_tage: Optional[int] = None
- last_entry_titel: Optional[str] = None
-
-@router.post("/abschied")
-async def ki_abschied(req: AbschiedRequest, request: Request,
- user=Depends(get_current_user)):
- """Persönlicher Abschiedstext — einmalig generiert, DB-gecacht."""
- with db() as conn:
- cached = conn.execute(
- "SELECT content FROM bday_ki_cache WHERE dog_id=? AND year=9999 AND mode='abschied'",
- (req.dog_id,)
- ).fetchone()
- if cached:
- return {"text": cached["content"], "cached": True}
-
- name = req.name.strip()[:40]
- rasse = req.rasse or ""
- km = f"{req.km_total:.0f} km" if req.km_total else None
- tage = f"{req.gemeinsam_tage} gemeinsame Tage" if req.gemeinsam_tage else None
- eintr = f"{req.diary_count} Tagebucheinträge" if req.diary_count else None
-
- stats_str = ", ".join(filter(None, [km, tage, eintr]))
- rasse_str = f" ({rasse})" if rasse else ""
-
- system = (
- "Du bist ein einfühlsamer Begleiter für Menschen in Trauer um ihren Hund. "
- "Schreibe warmherzig, persönlich und respektvoll auf Deutsch. "
- "Keine Floskeln, kein Kitsch — echte Wärme. "
- "Erwähne die Statistiken natürlich eingebunden."
- )
- prompt = (
- f"{name}{rasse_str} ist über die Regenbogenbrücke gegangen. "
- f"Schreibe einen kurzen, persönlichen Abschiedstext (ca. 80–100 Wörter) "
- f"der die Verbundenheit würdigt. "
- f"Statistiken: {stats_str or 'nicht bekannt'}. "
- f"Sei warm, nicht sentimental überladen. Schließe mit einem hoffnungsvollen Gedanken."
- )
-
- try:
- text = await ki_module.complete(
- system=system, prompt=prompt, max_tokens=300,
- requires_premium=False, user_id=user["id"],
- )
- with db() as conn:
- conn.execute(
- "INSERT OR REPLACE INTO bday_ki_cache (dog_id, year, mode, content) VALUES (?,9999,'abschied',?)",
- (req.dog_id, text)
- )
- return {"text": text, "cached": False}
- except Exception as e:
- raise HTTPException(503, str(e))
diff --git a/backend/routes/routen.py b/backend/routes/routen.py
index e1060ef..6755ccc 100644
--- a/backend/routes/routen.py
+++ b/backend/routes/routen.py
@@ -58,7 +58,6 @@ class RouteCreate(BaseModel):
is_public: Optional[bool] = False
hunde_tauglichkeit: Optional[str] = None # eingeschränkt | gut | sehr_gut | premium
client_time: Optional[str] = None
- dog_ids: Optional[List[int]] = None # Welche Hunde mitgegangen sind
class RouteUpdate(BaseModel):
name: Optional[str] = None
@@ -70,9 +69,6 @@ class RouteUpdate(BaseModel):
is_public: Optional[bool] = None
hunde_tauglichkeit: Optional[str] = None
-class RouteDogs(BaseModel):
- dog_ids: List[int]
-
def _simplify_track(track: list, max_pts: int = 40) -> list:
"""Reduziert GPS-Track auf max_pts Punkte für Vorschau."""
@@ -172,26 +168,7 @@ async def create_route(data: RouteCreate, user=Depends(get_current_user)):
int(data.is_public) if data.is_public is not None else 1,
data.hunde_tauglichkeit, is_valid, ct,
))
- route_id = cur.lastrowid
- row = conn.execute("SELECT * FROM routes WHERE id = ?", (route_id,)).fetchone()
-
- # Hunde zuordnen — entweder explizit oder alle Hunde des Users
- dog_ids = data.dog_ids or []
- if not dog_ids:
- # Fallback: alle Hunde des Users
- all_dogs = conn.execute(
- "SELECT id FROM dogs WHERE user_id=?", (user['id'],)
- ).fetchall()
- dog_ids = [d['id'] for d in all_dogs]
- for did in dog_ids:
- try:
- conn.execute(
- "INSERT OR IGNORE INTO route_dogs (route_id, dog_id) VALUES (?,?)",
- (route_id, did)
- )
- except Exception:
- pass
-
+ row = conn.execute("SELECT * FROM routes WHERE id = ?", (cur.lastrowid,)).fetchone()
update_streak(user['id'], conn)
check_and_award(user['id'], conn)
result = _parse(row)
@@ -218,10 +195,6 @@ async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)):
if not ORS_API_KEY:
raise HTTPException(503, "ORS nicht konfiguriert")
- from auth import has_pro_access
- if not has_pro_access(user):
- raise HTTPException(403, "Routenvorschläge sind ein Pro-Feature.")
-
is_privileged = (
user.get("rolle") in ("admin", "moderator") or
user.get("is_moderator") or
@@ -340,14 +313,9 @@ async def get_route(route_id: int):
"SELECT r.*, u.name AS user_name FROM routes r LEFT JOIN users u ON u.id = r.user_id WHERE r.id = ?",
(route_id,)
).fetchone()
- if not row:
- raise HTTPException(404, "Route nicht gefunden.")
- dog_rows = conn.execute(
- "SELECT dog_id FROM route_dogs WHERE route_id = ?", (route_id,)
- ).fetchall()
- result = _parse(row)
- result['dog_ids'] = [r['dog_id'] for r in dog_rows]
- return result
+ if not row:
+ raise HTTPException(404, "Route nicht gefunden.")
+ return _parse(row)
# ------------------------------------------------------------------
@@ -374,26 +342,6 @@ async def update_route(route_id: int, data: RouteUpdate, user=Depends(get_curren
return _parse(row)
-# ------------------------------------------------------------------
-# PATCH /api/routes/{id}/dogs — Hunde der Route aktualisieren
-# ------------------------------------------------------------------
-@router.patch("/{route_id}/dogs")
-async def update_route_dogs(route_id: int, data: RouteDogs, user=Depends(get_current_user)):
- with db() as conn:
- row = conn.execute("SELECT user_id FROM routes WHERE id = ?", (route_id,)).fetchone()
- if not row:
- raise HTTPException(404, "Route nicht gefunden.")
- if row['user_id'] != user['id']:
- raise HTTPException(403, "Nicht berechtigt.")
- conn.execute("DELETE FROM route_dogs WHERE route_id = ?", (route_id,))
- for did in data.dog_ids:
- conn.execute(
- "INSERT OR IGNORE INTO route_dogs (route_id, dog_id) VALUES (?, ?)",
- (route_id, did)
- )
- return {"ok": True}
-
-
# ------------------------------------------------------------------
# PATCH /api/routes/{id}/trim — Route kürzen (Datenschutz)
# ------------------------------------------------------------------
diff --git a/backend/routes/training.py b/backend/routes/training.py
index 263d90d..05e8e94 100644
--- a/backend/routes/training.py
+++ b/backend/routes/training.py
@@ -85,43 +85,28 @@ async def update_exercise(exercise_id: int, body: ExerciseUpdate, _=Depends(requ
# ------------------------------------------------------------------
class ProgressUpdate(BaseModel):
exercise_id: str
- status: Optional[str] = None
- dog_id: Optional[int] = None
+ status: Optional[str] = None # null/noch-nicht/manchmal/meistens/sitzt
@router.get("/progress")
-async def get_progress(dog_id: Optional[int] = None, user=Depends(get_current_user)):
+async def get_progress(user=Depends(get_current_user)):
uid = user["id"]
with db() as conn:
- if dog_id:
- rows = conn.execute(
- "SELECT exercise_id, status, updated_at FROM exercise_progress WHERE dog_id=?",
- (dog_id,)
- ).fetchall()
- else:
- rows = conn.execute(
- "SELECT exercise_id, status, updated_at FROM exercise_progress WHERE user_id=?",
- (uid,)
- ).fetchall()
+ rows = conn.execute(
+ "SELECT exercise_id, status, updated_at FROM exercise_progress WHERE user_id=?",
+ (uid,)
+ ).fetchall()
return [dict(r) for r in rows]
@router.post("/progress")
async def upsert_progress(body: ProgressUpdate, user=Depends(get_current_user)):
uid = user["id"]
with db() as conn:
- if body.dog_id:
- conn.execute("""
- INSERT INTO exercise_progress (user_id, dog_id, exercise_id, status)
- VALUES (?,?,?,?)
- ON CONFLICT(dog_id, exercise_id) DO UPDATE
- SET status=excluded.status, updated_at=datetime('now')
- """, (uid, body.dog_id, body.exercise_id, body.status))
- else:
- conn.execute("""
- INSERT INTO exercise_progress (user_id, exercise_id, status)
- VALUES (?,?,?)
- ON CONFLICT(dog_id, exercise_id) DO UPDATE
- SET status=excluded.status, updated_at=datetime('now')
- """, (uid, body.exercise_id, body.status))
+ conn.execute("""
+ INSERT INTO exercise_progress (user_id, exercise_id, status)
+ VALUES (?,?,?)
+ ON CONFLICT(user_id, exercise_id) DO UPDATE
+ SET status=excluded.status, updated_at=datetime('now')
+ """, (uid, body.exercise_id, body.status))
return {"ok": True}
# ------------------------------------------------------------------
@@ -130,22 +115,15 @@ async def upsert_progress(body: ProgressUpdate, user=Depends(get_current_user)):
class PlanProgress(BaseModel):
item_key: str
checked: bool
- dog_id: Optional[int] = None
@router.get("/plan-progress")
-async def get_plan_progress(dog_id: Optional[int] = None, user=Depends(get_current_user)):
+async def get_plan_progress(user=Depends(get_current_user)):
uid = user["id"]
with db() as conn:
- if dog_id:
- rows = conn.execute(
- "SELECT item_key, checked FROM training_plan_progress WHERE dog_id=?",
- (dog_id,)
- ).fetchall()
- else:
- rows = conn.execute(
- "SELECT item_key, checked FROM training_plan_progress WHERE user_id=?",
- (uid,)
- ).fetchall()
+ rows = conn.execute(
+ "SELECT item_key, checked FROM training_plan_progress WHERE user_id=?",
+ (uid,)
+ ).fetchall()
return [dict(r) for r in rows]
@router.post("/plan-progress")
@@ -154,13 +132,13 @@ async def upsert_plan_progress(body: PlanProgress, user=Depends(get_current_user
with db() as conn:
if body.checked:
conn.execute("""
- INSERT OR REPLACE INTO training_plan_progress (user_id, dog_id, item_key, checked)
- VALUES (?,?,?,1)
- """, (uid, body.dog_id, body.item_key))
+ INSERT OR REPLACE INTO training_plan_progress (user_id, item_key, checked)
+ VALUES (?,?,1)
+ """, (uid, body.item_key))
else:
conn.execute(
- "DELETE FROM training_plan_progress WHERE dog_id=? AND item_key=?",
- (body.dog_id, body.item_key)
+ "DELETE FROM training_plan_progress WHERE user_id=? AND item_key=?",
+ (uid, body.item_key)
)
return {"ok": True}
@@ -171,19 +149,13 @@ GRUNDKOMMANDOS_ORDER = ['Sitz', 'Platz', 'Bleib', 'Hier / Komm', 'Fuß', 'Aus /
TRICKS_FIRST = ['Pfote / Schütteln', 'Dreh', 'Auf die Decke', 'Nasenarbeit / Suchen']
@router.get("/suggestions")
-async def get_suggestions(dog_id: Optional[int] = None, user=Depends(get_current_user)):
+async def get_suggestions(user=Depends(get_current_user)):
uid = user["id"]
with db() as conn:
- if dog_id:
- rows = conn.execute(
- "SELECT exercise_id, status FROM exercise_progress WHERE dog_id=?",
- (dog_id,)
- ).fetchall()
- else:
- rows = conn.execute(
- "SELECT exercise_id, status FROM exercise_progress WHERE user_id=?",
- (uid,)
- ).fetchall()
+ rows = conn.execute(
+ "SELECT exercise_id, status FROM exercise_progress WHERE user_id=?",
+ (uid,)
+ ).fetchall()
progress = {r["exercise_id"]: r["status"] for r in rows}
diff --git a/backend/routes/weather.py b/backend/routes/weather.py
index 0bd757a..2167b19 100644
--- a/backend/routes/weather.py
+++ b/backend/routes/weather.py
@@ -3,7 +3,6 @@ BAN YARO — Wetter-API
GET /api/weather?lat=&lon= → aktuelles Wetter + Zecken-Warnung für Nutzerstandort
"""
-import os
import json
from fastapi import APIRouter, Query, HTTPException, Depends
import weather as weather_module
@@ -12,34 +11,6 @@ from database import db
router = APIRouter()
-OWM_API_KEY = os.getenv("OPENWEATHERMAP_KEY", "")
-
-
-ALLOWED_OWM_LAYERS = {"temp_new", "clouds_new", "wind_new", "pressure_new", "precipitation_new"}
-
-
-@router.get('/radar-tiles')
-async def radar_tile_config(user=Depends(get_current_user)):
- """Regenradar-Tile-Config (RainViewer)."""
- return {"provider": "rainviewer"}
-
-
-@router.get('/layer-tiles')
-async def layer_tile_config(
- layer: str = "temp_new",
- user=Depends(get_current_user),
-):
- """OWM-Tile-URL für Wetter-Layer (Key bleibt server-seitig)."""
- if layer not in ALLOWED_OWM_LAYERS:
- raise HTTPException(400, f"Unbekannter Layer. Erlaubt: {', '.join(ALLOWED_OWM_LAYERS)}")
- if not OWM_API_KEY:
- raise HTTPException(503, "OWM nicht konfiguriert.")
- return {
- "url": f"https://tile.openweathermap.org/map/{layer}/{{z}}/{{x}}/{{y}}.png?appid={OWM_API_KEY}",
- "maxNativeZoom": 18,
- "opacity": 0.6,
- }
-
@router.get('')
async def get_weather(
diff --git a/backend/static/css/components.css b/backend/static/css/components.css
index cb54b88..49d89ab 100644
--- a/backend/static/css/components.css
+++ b/backend/static/css/components.css
@@ -3184,16 +3184,6 @@ html.modal-open {
color: #fff;
font-size: 1rem;
}
-#map-radar-btn.active {
- background: #1d4ed8;
- border-color: #1d4ed8;
- color: #fff;
-}
-#map-temp-btn.active {
- background: #dc2626;
- border-color: #dc2626;
- color: #fff;
-}
.map-fab:disabled { opacity: 0.5; cursor: default; }
.map-fab--offline.loading { animation: fab-spin 1.2s linear infinite; pointer-events: none; }
@keyframes fab-spin { to { transform: rotate(360deg); } }
diff --git a/backend/static/index.html b/backend/static/index.html
index f616f14..cf2117f 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -101,9 +101,9 @@
-
-
-
+
+
+
@@ -583,10 +583,10 @@
-
-
-
-
+
+
+
+
diff --git a/backend/static/js/api.js b/backend/static/js/api.js
index 5781a62..893f1b4 100644
--- a/backend/static/js/api.js
+++ b/backend/static/js/api.js
@@ -58,10 +58,7 @@ const API = (() => {
try { data = await response.json(); } catch { data = null; }
if (!response.ok) {
- const _d = data?.detail;
- const message = (typeof _d === 'string' ? _d
- : Array.isArray(_d) ? (_d[0]?.msg || 'Ungültige Eingabe')
- : null) || data?.message || `Fehler ${response.status}`;
+ const message = data?.detail || data?.message || `Fehler ${response.status}`;
const isSwOffline = response.status === 503 && message.startsWith('Offline');
// Retry: GET auf echte 5xx (nicht SW-generierte Offline-503)
@@ -142,15 +139,6 @@ const API = (() => {
deletePhoto(id) { return del(`/dogs/${id}/photo`); },
getSkills(id) { return get(`/dogs/${id}/skills`); },
welcomeDashboard(dogId) { return get(`/dogs/${dogId}/welcome-dashboard`); },
- gedenken(id, datum) { return post(`/dogs/${id}/gedenken`, { verstorben_am: datum }); },
- gedenkseite(id) { return get(`/dogs/${id}/gedenkseite`); },
- futterList(id) { return get(`/dogs/${id}/futter`); },
- futterCreate(id, data) { return post(`/dogs/${id}/futter`, data); },
- futterDelete(id, eid) { return del(`/dogs/${id}/futter/${eid}`); },
- reaktionList(id) { return get(`/dogs/${id}/futter/reaktionen`); },
- reaktionCreate(id, d) { return post(`/dogs/${id}/futter/reaktion`, d); },
- reaktionDelete(id, rid) { return del(`/dogs/${id}/futter/reaktion/${rid}`); },
- futterAnalyse(id) { return get(`/dogs/${id}/futter/analyse`); },
};
// ----------------------------------------------------------
@@ -296,7 +284,6 @@ const API = (() => {
rate(id, wertung) { return post(`/routes/${id}/rate`, { wertung }); },
walked(id, walked_km, progress_pct) { return post(`/routes/${id}/walked`, { walked_km, progress_pct }); },
reverse(id) { return post(`/routes/${id}/reverse`, {}); },
- updateDogs(id, dog_ids) { return patch(`/routes/${id}/dogs`, { dog_ids }); },
addPhoto(id, file) {
const fd = new FormData();
fd.append('file', file);
@@ -310,11 +297,11 @@ const API = (() => {
// TRAINING & ÜBUNGSFORTSCHRITT
// ----------------------------------------------------------
const training = {
- getProgress(dogId) { return get(`/training/progress${dogId ? `?dog_id=${dogId}` : ''}`); },
- setProgress(id, status, dogId){ return post('/training/progress', { exercise_id: id, status, dog_id: dogId || null }); },
- getSuggestions(dogId) { return get(`/training/suggestions${dogId ? `?dog_id=${dogId}` : ''}`); },
- getPlanProgress(dogId) { return get(`/training/plan-progress${dogId ? `?dog_id=${dogId}` : ''}`); },
- setPlanProgress(key, checked, dogId) { return post('/training/plan-progress', { item_key: key, checked, dog_id: dogId || null }); },
+ getProgress() { return get('/training/progress'); },
+ setProgress(id, status) { return post('/training/progress', { exercise_id: id, status }); },
+ getSuggestions() { return get('/training/suggestions'); },
+ getPlanProgress() { return get('/training/plan-progress'); },
+ setPlanProgress(key, checked) { return post('/training/plan-progress', { item_key: key, checked }); },
getRecommendations(dogId) { return get(`/training/recommendations?dog_id=${dogId}`); },
};
@@ -474,7 +461,6 @@ const API = (() => {
// ----------------------------------------------------------
const friends = {
list() { return get('/friends/'); },
- pending() { return get('/friends/pending'); },
search(q) { return get(`/friends/search?q=${encodeURIComponent(q)}`); },
activity() { return get('/friends/activity'); },
sendRequest(userId) { return post(`/friends/request/${userId}`, {}); },
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index e732f44..db6b183 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,8 +3,8 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '872'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
-const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
+const APP_VER = '826'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
if (location.search.includes('_t=')) history.replaceState(null, '', '/');
@@ -57,7 +57,7 @@ const App = (() => {
'erste-hilfe': { title: 'Erste Hilfe', module: null },
settings: { title: 'Einstellungen', module: null },
lost: { title: 'Verlorener Hund', module: null },
- friends: { title: 'Freunde', module: null, requiresAuth: true },
+ friends: { title: 'Freunde', module: null, requiresAuth: true, requiresPro: true },
chat: { title: 'Nachrichten', module: null, requiresAuth: true, requiresPro: true },
social: { title: 'Social Media', module: null, requiresAuth: true },
admin: { title: 'Admin', module: null, requiresAuth: true },
@@ -198,26 +198,6 @@ const App = (() => {
return;
}
- // Pro-Feature-Hinweis für Admins/Mods/Manager — Banner VOR .page-body, überlebt page.module.init()
- const pageEl = document.getElementById(`page-${pageId}`);
- if (pageEl) {
- pageEl.querySelector('#pro-role-banner')?.remove();
- if (page.requiresPro && state.user) {
- const t = state.user.subscription_tier || 'standard';
- const isRoleBased = !t.endsWith('_test') && !['pro','breeder'].includes(t) &&
- (state.user.rolle === 'admin' || state.user.rolle === 'moderator' ||
- state.user.is_moderator || state.user.is_social_media);
- if (isRoleBased) {
- const banner = document.createElement('div');
- banner.id = 'pro-role-banner';
- banner.style.cssText = 'background:#92400e;color:#fef3c7;padding:8px 16px;font-size:12px;font-weight:600;display:flex;align-items:center;gap:8px;';
- banner.innerHTML = `
- ⭐ Pro-Feature — Standard-User sehen diese Seite nicht`;
- pageEl.insertBefore(banner, pageEl.firstChild);
- }
- }
- }
-
if (page.module) {
const hasParams = params && Object.keys(params).length > 0;
if (hasParams) {
@@ -905,12 +885,6 @@ const App = (() => {
if (!dog || dog.id === state.activeDog?.id) return;
state.activeDog = dog;
localStorage.setItem('by_active_dog', String(dogId));
- // SW-Cache für hund-spezifische Daten invalidieren
- navigator.serviceWorker?.controller?.postMessage({
- type: 'INVALIDATE_CACHE',
- paths: ['/api/training/progress', '/api/training/plan-progress',
- '/api/training/suggestions', `/api/dogs/${dogId}/welcome-dashboard`],
- });
_renderDogSwitcher();
_notifyDogChange();
}
diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js
index 2747256..0dfa8c7 100644
--- a/backend/static/js/pages/admin.js
+++ b/backend/static/js/pages/admin.js
@@ -25,7 +25,6 @@ window.Page_admin = (() => {
{ id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' },
{ id: 'hilfe', label: 'Hilfe/FAQ', icon: 'question' },
{ id: 'uebungen_admin', label: 'Übungen', icon: 'barbell' },
- { id: 'referrals', label: 'Referrals', icon: 'share-network' },
];
// ------------------------------------------------------------------
@@ -162,7 +161,6 @@ window.Page_admin = (() => {
case 'bewerbungen': await _renderBewerbungen(el); break;
case 'hilfe': await _renderHilfe(el); break;
case 'uebungen_admin': await _renderUebungenAdmin(el); break;
- case 'referrals': await _renderReferrals(el); break;
}
} catch (e) {
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
@@ -536,22 +534,22 @@ window.Page_admin = (() => {
};
el.innerHTML = `
-
- ${_statCard('users', 'Nutzer gesamt', s.users_total, 'var(--c-primary)', 'nutzer')}
- ${_statCard('user-plus', 'Neu heute', s.users_today, 'var(--c-success)', 'nutzer')}
- ${_statCard('activity', 'Aktiv (7 Tage)', s.active_users_7d, 'var(--c-primary)', 'nutzer')}
- ${_statCard('paw-print', 'Hunde', s.dogs_total, 'var(--c-primary)', 'nutzer')}
- ${_statCard('chat-circle-dots','Threads', s.threads, 'var(--c-text-secondary)','forum')}
- ${_statCard('warning', 'Offene Meldungen', s.open_reports, s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)', 'moderation')}
- ${_statCard('camera', 'Fotos freizugeben', s.pending_fotos ?? 0, (s.pending_fotos ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)', 'moderation')}
- ${_statCard('skull', 'Gesperrte User', s.banned, s.banned > 0 ? '#f59e0b' : 'var(--c-text-muted)', 'nutzer')}
- ${_statCard('warning-octagon', 'Giftk. aktiv', s.poison_active, 'var(--c-danger)', 'system')}
- ${_statCard('bell', 'Push-Abos', s.push_subscriptions, 'var(--c-text-secondary)','system')}
+
+ ${_statCard('users', 'Nutzer gesamt', s.users_total, 'var(--c-primary)')}
+ ${_statCard('user-plus', 'Neu heute', s.users_today, 'var(--c-success)')}
+ ${_statCard('activity', 'Aktiv (7 Tage)', s.active_users_7d, 'var(--c-primary)')}
+ ${_statCard('paw-print', 'Hunde', s.dogs_total, 'var(--c-primary)')}
+ ${_statCard('chat-circle-dots','Threads', s.threads, 'var(--c-text-secondary)')}
+ ${_statCard('warning', 'Offene Meldungen', s.open_reports, s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)')}
+ ${_statCard('camera', 'Fotos freizugeben', s.pending_fotos ?? 0, (s.pending_fotos ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}
+ ${_statCard('skull', 'Gesperrte User', s.banned, s.banned > 0 ? '#f59e0b' : 'var(--c-text-muted)')}
+ ${_statCard('warning-octagon', 'Giftk. aktiv', s.poison_active, 'var(--c-danger)')}
+ ${_statCard('bell', 'Push-Abos', s.push_subscriptions, 'var(--c-text-secondary)')}
${_statCard('image', 'Media-Einträge', s.media_count, 'var(--c-text-secondary)')}
${_statCard('map-pin', 'Routen', s.routes_total, 'var(--c-text-secondary)')}
${_statCard('calendar', 'Events', s.events_total, 'var(--c-text-secondary)')}
- ${_statCard('map-trifold', 'OSM-Marker', s.osm_total.toLocaleString('de'), 'var(--c-success)', 'system')}
- ${_statCard('squares-four', 'Gecachte Tiles', s.osm_tiles.toLocaleString('de'), 'var(--c-text-secondary)', 'system')}
+ ${_statCard('map-trifold', 'OSM-Marker', s.osm_total.toLocaleString('de'), 'var(--c-success)')}
+ ${_statCard('squares-four', 'Gecachte Tiles', s.osm_tiles.toLocaleString('de'), 'var(--c-text-secondary)')}
@@ -707,19 +705,11 @@ window.Page_admin = (() => {
`;
-
- el.querySelector('#adm-overview-grid')?.addEventListener('click', e => {
- const card = e.target.closest('[data-adm-tab]');
- if (!card) return;
- const tab = card.dataset.admTab;
- _container.querySelector(`#adm-tabs .by-tab[data-tab="${tab}"]`)?.click();
- });
}
- function _statCard(icon, label, value, color, tab = null) {
- const clickable = tab ? `data-adm-tab="${tab}" style="padding:var(--space-4);text-align:center;cursor:pointer"` : `style="padding:var(--space-4);text-align:center"`;
+ function _statCard(icon, label, value, color) {
return `
-
+