diff --git a/backend/database.py b/backend/database.py index 5e38a96..efb9266 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2058,3 +2058,214 @@ def _migrate(conn_factory): ON gassi_zeiten(user_id, aktiv); """) logger.info("Migration: Gassi-Zeiten-Tabelle bereit.") + + # ---- Feature: Hilfe/FAQ ---- + conn.executescript(""" + CREATE TABLE IF NOT EXISTS help_articles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + kategorie TEXT NOT NULL, + frage TEXT NOT NULL, + antwort TEXT NOT NULL, + sort_order INTEGER DEFAULT 0, + aktiv INTEGER DEFAULT 1, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE INDEX IF NOT EXISTS idx_help_kat ON help_articles(kategorie, sort_order); + """) + _seed_help_articles(conn) + logger.info("Migration: Hilfe/FAQ-Tabelle bereit.") + + +def _seed_help_articles(conn): + """Befüllt help_articles mit Starter-FAQs — nur wenn die Tabelle noch leer ist.""" + count = conn.execute("SELECT COUNT(*) FROM help_articles").fetchone()[0] + if count > 0: + return + + _SEED = [ + # ── installation ────────────────────────────────────────────── + ("installation", "Was ist eine PWA?", + "Eine Progressive Web App (PWA) ist eine Website, die sich wie eine richtige App verhält. " + "Du kannst Ban Yaro direkt im Browser nutzen oder als App auf deinem Startbildschirm speichern — " + "ohne den App Store.\n\n" + "Vorteile: immer aktuell, kein Download, funktioniert auch offline.", + 1), + ("installation", "Warum ist Ban Yaro nicht im App Store?", + "Als PWA können wir Ban Yaro viel schneller mit neuen Funktionen ausstatten und Fehler sofort " + "beheben. App-Store-Apps müssen oft tagelang auf Freigabe warten.\n\n" + "Außerdem sparst du Speicherplatz auf deinem Handy — Ban Yaro braucht nur wenige Megabyte.", + 2), + ("installation", "iPhone: Wie füge ich Ban Yaro zum Startbildschirm hinzu?", + "1. Öffne banyaro.app in Safari (nicht Chrome oder Firefox).\n" + "2. Tippe auf das Teilen-Symbol (Quadrat mit Pfeil nach oben) unten in der Mitte.\n" + "3. Scrolle im Menü nach unten und wähle 'Zum Home-Bildschirm'.\n" + "4. Bestätige mit 'Hinzufügen'.\n\n" + "Ban Yaro erscheint jetzt als App-Icon auf deinem Startbildschirm.", + 3), + ("installation", "Android: Wie füge ich Ban Yaro zum Startbildschirm hinzu?", + "1. Öffne banyaro.app in Chrome.\n" + "2. Tippe oben rechts auf die drei Punkte (Menü).\n" + "3. Wähle 'App installieren' oder 'Zum Startbildschirm hinzufügen'.\n" + "4. Bestätige die Installation.\n\n" + "Bei manchen Android-Geräten erscheint auch automatisch ein Banner am unteren Bildschirmrand.", + 4), + ("installation", "Wie aktualisiere ich die App?", + "Ban Yaro aktualisiert sich automatisch im Hintergrund. Wenn ein Update bereit ist, " + "siehst du einen Hinweis in der App.\n\n" + "Falls du Probleme hast und die App sich komisch verhält: Einstellungen öffnen, " + "ganz unten 'Auf Update prüfen' tippen. Danach die App schließen und neu öffnen.", + 5), + + # ── erste_schritte ──────────────────────────────────────────── + ("erste_schritte", "Wie lege ich meinen Hund an?", + "Tippe in der Navigation unten auf den Hund-Tab (HUND) und dann auf 'Hund hinzufügen'. " + "Du gibst Name, Rasse und Geburtsdatum ein — ein Foto ist optional, macht aber gleich mehr Spaß.\n\n" + "Du kannst mehrere Hunde anlegen und zwischen ihnen wechseln.", + 1), + ("erste_schritte", "Wie navigiere ich zwischen den Welten?", + "Ban Yaro ist in drei Welten aufgeteilt: JETZT, HUND und WELT. " + "Du kannst links-rechts wischen oder die drei Buttons unten in der Navigation tippen.\n\n" + "JETZT zeigt dir alles, was gerade relevant ist. HUND ist alles über deinen Vierbeiner. " + "WELT verbindet dich mit der Hunde-Community.", + 2), + ("erste_schritte", "Was bedeuten JETZT, HUND und WELT?", + "JETZT: Dein persönliches Dashboard — Wetter, Gassi-Planer, aktuelle Hinweise in deiner Nähe.\n\n" + "HUND: Alles rund um deinen Hund — Tagebuch, Gesundheit, Training, Ernährung, Tierarzt.\n\n" + "WELT: Die Community — Forum, Wiki, Events, andere Hundebesitzer, Gassi-Treffen, Karte.", + 3), + ("erste_schritte", "Wie passe ich die Chips auf der JETZT-Seite an?", + "Tippe oben rechts auf der JETZT-Seite auf das Zahnrad-Symbol. " + "Dort kannst du auswählen, welche Funktions-Chips du sehen möchtest " + "und in welcher Reihenfolge sie angezeigt werden.", + 4), + ("erste_schritte", "Wie schreibe ich mein erstes Tagebuch?", + "Gehe in die HUND-Welt und tippe auf 'Tagebuch'. " + "Mit dem Plus-Button kannst du einen neuen Eintrag anlegen.\n\n" + "Du kannst Text schreiben, Fotos hinzufügen, deine Stimmung eintragen und den Ort markieren. " + "Alle Einträge sind nur für dich sichtbar.", + 5), + + # ── standort ────────────────────────────────────────────────── + ("standort", "Wie gebe ich den Standort auf dem iPhone frei?", + "1. Öffne die Einstellungen deines iPhones.\n" + "2. Scrolle zu Safari.\n" + "3. Tippe auf 'Standort'.\n" + "4. Wähle 'Beim Benutzen der App erlauben'.\n\n" + "Alternativ: Wenn Ban Yaro fragt, ob es deinen Standort nutzen darf, " + "tippe auf 'Erlauben'. Diese Abfrage erscheint beim ersten Öffnen der Karte oder Wetter-Funktion.", + 1), + ("standort", "Wie gebe ich den Standort auf Android frei?", + "1. Öffne die Einstellungen deines Handys.\n" + "2. Gehe zu Apps → Chrome → Berechtigungen → Standort.\n" + "3. Wähle 'Beim Benutzen der App erlauben'.\n\n" + "Beim ersten Öffnen der Karte oder Wetter-Funktion fragt Ban Yaro automatisch nach der Berechtigung.", + 2), + ("standort", "Der Standort ist blockiert — wie setze ich das zurück?", + "Wenn du versehentlich 'Ablehnen' getippt hast, kannst du das zurücksetzen:\n\n" + "iPhone: Einstellungen → Safari → Standort → 'Beim Benutzen erlauben'\n\n" + "Android: In Chrome tippe auf das Schloss-Symbol links in der Adresszeile → " + "Website-Einstellungen → Standort → Erlauben.\n\n" + "Tipp: Manchmal hilft es, die Website-Daten zu löschen und Ban Yaro neu zu öffnen.", + 3), + ("standort", "Das Wetter lädt nicht — was kann ich tun?", + "Das Wetter benötigt deinen aktuellen Standort. Prüfe zuerst, ob die Standort-Berechtigung " + "erteilt ist (siehe 'Standort freigeben').\n\n" + "Falls es trotzdem nicht klappt: Schließe Ban Yaro vollständig und öffne es erneut. " + "Bei anhaltenden Problemen tippe in den Einstellungen auf 'Auf Update prüfen'.", + 4), + + # ── account ─────────────────────────────────────────────────── + ("account", "Ich habe mein Passwort vergessen — was nun?", + "Auf der Anmeldeseite findest du den Link 'Passwort vergessen'. " + "Gib dort deine E-Mail-Adresse ein — du erhältst innerhalb weniger Minuten " + "einen Link zum Zurücksetzen.\n\n" + "Schau auch im Spam-Ordner, falls die E-Mail nicht ankommt.", + 1), + ("account", "Die E-Mail-Bestätigung ist nicht angekommen.", + "Bitte prüfe deinen Spam- oder Junk-Ordner. E-Mails von Ban Yaro kommen von noreply@banyaro.app.\n\n" + "Falls du die E-Mail dort nicht findest, kannst du die Bestätigung in den Einstellungen " + "unter 'Konto' erneut anfordern.\n\n" + "Stelle sicher, dass deine E-Mail-Adresse korrekt geschrieben ist.", + 2), + ("account", "Wie kann ich mein Konto löschen?", + "Gehe in die Einstellungen (Zahnrad-Symbol) → Konto → 'Konto löschen'.\n\n" + "Achtung: Die Löschung ist endgültig. Alle deine Daten, Hunde-Profile und " + "Tagebuch-Einträge werden dauerhaft entfernt. Diese Aktion kann nicht rückgängig gemacht werden.", + 3), + ("account", "Wie ändere ich meine E-Mail-Adresse?", + "Das Ändern der E-Mail-Adresse ist aus Sicherheitsgründen aktuell nur über den " + "Support möglich. Schreibe uns an support@banyaro.app mit deiner aktuellen und " + "gewünschten neuen E-Mail-Adresse.", + 4), + + # ── features ────────────────────────────────────────────────── + ("features", "Was ist der Gassi-Score?", + "Der Gassi-Score zeigt dir auf einen Blick, wie gut das Wetter gerade für einen Spaziergang ist. " + "Er berücksichtigt Temperatur, Regen, Wind, UV-Index und — bei heißem Wetter — " + "die Asphalt-Temperatur.\n\n" + "Grün = super. Gelb = geht noch. Rot = lieber warten oder kürzer machen.", + 1), + ("features", "Was kann der KI-Tierarzt?", + "Der KI-Tierarzt beantwortet allgemeine Fragen rund um die Gesundheit deines Hundes — " + "zum Beispiel zu Symptomen, Ernährung oder Verhalten.\n\n" + "Wichtig: Er ersetzt keinen echten Tierarzt. Bei ernsten Symptomen oder Notfällen " + "wende dich bitte sofort an einen Tierarzt in deiner Nähe.", + 2), + ("features", "Wie funktioniert der Offline-Modus?", + "Ban Yaro speichert die wichtigsten Funktionen lokal auf deinem Gerät. " + "Auch ohne Internet kannst du dein Tagebuch lesen, Einträge anlegen und " + "gespeicherte Karten nutzen.\n\n" + "Neue Daten werden automatisch synchronisiert, sobald du wieder online bist.", + 3), + ("features", "Wie richte ich Push-Benachrichtigungen ein?", + "Gehe in die Einstellungen (Zahnrad-Symbol) und tippe auf 'Push-Benachrichtigungen'. " + "Dort kannst du auswählen, für welche Ereignisse du Benachrichtigungen erhalten möchtest — " + "z.B. Giftköder-Warnungen, neue Nachrichten oder Gassi-Erinnerungen.\n\n" + "Auf dem iPhone muss Ban Yaro als App auf dem Startbildschirm installiert sein, " + "damit Push-Nachrichten funktionieren.", + 4), + ("features", "Kann ich mein Tagebuch-Hintergrundbild ändern?", + "Ja! Im Tagebuch tippe oben auf das Bild-Symbol oder auf 'Hintergrund anpassen'. " + "Du kannst ein eigenes Foto wählen oder eines der vorhandenen Motive nutzen.\n\n" + "Das Hintergrundbild gilt für alle Einträge und macht dein Tagebuch ganz persönlich.", + 5), + ("features", "Was ist der Giftköder-Alarm?", + "Der Giftköder-Alarm zeigt dir gemeldete Giftköder in deiner Nähe auf einer Karte. " + "Du kannst selbst Funde melden und andere Hundebesitzer warnen.\n\n" + "In den Einstellungen kannst du Push-Benachrichtigungen aktivieren, " + "damit du sofort gewarnt wirst, wenn in deiner Nähe ein Giftköder gemeldet wird.", + 6), + + # ── probleme ────────────────────────────────────────────────── + ("probleme", "Die App zeigt alte Daten — was tun?", + "Tippe in den Einstellungen ganz unten auf 'Auf Update prüfen'. " + "Danach schließe Ban Yaro vollständig (App aus dem Multitasking entfernen) " + "und öffne sie erneut.\n\n" + "Falls das nicht hilft: Lösche die Website-Daten in deinem Browser und öffne " + "Ban Yaro erneut. Du bleibst dabei angemeldet.", + 1), + ("probleme", "Das Wetter lädt nicht.", + "Stelle sicher, dass die Standort-Berechtigung erteilt ist. " + "Ohne Standort kann Ban Yaro kein lokales Wetter laden.\n\n" + "Prüfe außerdem deine Internetverbindung. Bei schlechtem WLAN oder Mobilfunk " + "kann es zu Verzögerungen kommen. Eine kurze Wartezeit und erneutes Tippen hilft meist.", + 2), + ("probleme", "Die App reagiert nicht oder friert ein.", + "Schließe Ban Yaro vollständig (aus dem Multitasking entfernen) und öffne sie erneut.\n\n" + "Falls das Problem anhält, prüfe ob dein Gerät ausreichend Speicher hat. " + "Starte dein Handy neu — das löst in den meisten Fällen temporäre Hänger.", + 3), + ("probleme", "Wie melde ich einen Fehler?", + "Wir freuen uns über Feedback! Schreibe uns an support@banyaro.app mit einer kurzen " + "Beschreibung des Problems.\n\n" + "Hilfreich sind: Was hast du getan? Was hast du erwartet? Was ist stattdessen passiert? " + "Welches Gerät und Browser nutzt du?\n\n" + "Wir melden uns so schnell wie möglich.", + 4), + ] + + for kat, frage, antwort, sort in _SEED: + conn.execute( + "INSERT INTO help_articles (kategorie, frage, antwort, sort_order) VALUES (?, ?, ?, ?)", + (kat, frage, antwort, sort), + ) diff --git a/backend/main.py b/backend/main.py index a883e9b..dff2344 100644 --- a/backend/main.py +++ b/backend/main.py @@ -235,6 +235,7 @@ from routes.playdate import router as playdate_router 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 app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) @@ -296,6 +297,7 @@ app.include_router(playdate_router, prefix="/api/playdate", ta app.include_router(ernaehrung_router, prefix="/api/dogs", tags=["Ernährung"]) 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"]) # ------------------------------------------------------------------ @@ -325,7 +327,7 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") os.makedirs(MEDIA_DIR, exist_ok=True) app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") -APP_VER = "715" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "727" # muss mit APP_VER in app.js übereinstimmen @app.get("/api/version") async def app_version(): diff --git a/backend/routes/achievements.py b/backend/routes/achievements.py index e8d0cba..bff2421 100644 --- a/backend/routes/achievements.py +++ b/backend/routes/achievements.py @@ -308,8 +308,8 @@ async def my_achievements(user=Depends(get_current_user)): # 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 = ? + JOIN dogs dog ON dog.id = d.dog_id + WHERE dog.user_id = ? AND d.weather_json IS NOT NULL AND ( CAST(json_extract(d.weather_json, '$.precip_prob') AS INTEGER) > 60 @@ -321,20 +321,25 @@ async def my_achievements(user=Depends(get_current_user)): # 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) + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id + WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) AS jahreszeiten_score FROM (SELECT 1) """, (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 + SELECT COUNT(*) AS cnt FROM diary d + JOIN dogs dog ON dog.id = d.dog_id + WHERE dog.user_id = ? + AND d.weather_json IS NOT NULL + AND CAST(json_extract(d.weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 """, (uid,)).fetchone() earned_rows = conn.execute( diff --git a/backend/routes/help.py b/backend/routes/help.py new file mode 100644 index 0000000..4c77018 --- /dev/null +++ b/backend/routes/help.py @@ -0,0 +1,98 @@ +"""BAN YARO — Hilfe / FAQ Routes""" + +from fastapi import APIRouter, Depends, Query +from pydantic import BaseModel +from typing import Optional +from database import db +from auth import get_current_user_optional, require_admin + +router = APIRouter() + + +# ------------------------------------------------------------------ +# Schemas +# ------------------------------------------------------------------ +class ArticleCreate(BaseModel): + kategorie: str + frage: str + antwort: str + sort_order: int = 0 + aktiv: int = 1 + + +class ArticleUpdate(BaseModel): + kategorie: Optional[str] = None + frage: Optional[str] = None + antwort: Optional[str] = None + sort_order: Optional[int] = None + aktiv: Optional[int] = None + + +# ------------------------------------------------------------------ +# GET /api/help — öffentlich (nur aktive); ?all=1 für Admins +# ------------------------------------------------------------------ +@router.get("") +def get_help( + all: int = Query(0), + user=Depends(get_current_user_optional), +): + is_admin = user and user.get("rolle") == "admin" + show_all = all == 1 and is_admin + + with db() as conn: + if show_all: + rows = conn.execute( + "SELECT id, kategorie, frage, antwort, sort_order, aktiv " + "FROM help_articles " + "ORDER BY kategorie, sort_order, id" + ).fetchall() + else: + rows = conn.execute( + "SELECT id, kategorie, frage, antwort, sort_order, aktiv " + "FROM help_articles " + "WHERE aktiv = 1 " + "ORDER BY kategorie, sort_order, id" + ).fetchall() + + return [dict(r) for r in rows] + + +# ------------------------------------------------------------------ +# POST /api/help — Admin: neuen Artikel anlegen +# ------------------------------------------------------------------ +@router.post("", status_code=201) +def create_article(body: ArticleCreate, admin=Depends(require_admin)): + with db() as conn: + cur = conn.execute( + "INSERT INTO help_articles (kategorie, frage, antwort, sort_order, aktiv) " + "VALUES (?, ?, ?, ?, ?)", + (body.kategorie, body.frage, body.antwort, body.sort_order, body.aktiv), + ) + return {"ok": True, "id": cur.lastrowid} + + +# ------------------------------------------------------------------ +# PATCH /api/help/{article_id} — Admin: Artikel bearbeiten +# ------------------------------------------------------------------ +@router.patch("/{article_id}") +def update_article(article_id: int, body: ArticleUpdate, admin=Depends(require_admin)): + updates = {k: v for k, v in body.model_dump(exclude_none=True).items()} + if not updates: + return {"ok": True} + set_clause = ", ".join(f"{k}=?" for k in updates) + with db() as conn: + conn.execute( + f"UPDATE help_articles SET {set_clause} WHERE id=?", + (*updates.values(), article_id), + ) + return {"ok": True} + + +# ------------------------------------------------------------------ +# DELETE /api/help/{article_id} — Admin: Artikel löschen +# ------------------------------------------------------------------ +@router.delete("/{article_id}") +def delete_article(article_id: int, admin=Depends(require_admin)): + with db() as conn: + conn.execute("DELETE FROM help_articles WHERE id=?", (article_id,)) + return {"ok": True} diff --git a/backend/routes/training.py b/backend/routes/training.py index 48e37fe..05e8e94 100644 --- a/backend/routes/training.py +++ b/backend/routes/training.py @@ -6,7 +6,7 @@ from typing import Optional import datetime import ki from database import db -from auth import get_current_user +from auth import get_current_user, require_admin router = APIRouter() @@ -50,6 +50,36 @@ async def get_exercises(): }) return by_tab +# ------------------------------------------------------------------ +# Admin: Übung bearbeiten (beschreibung / schritte / tipp) +# ------------------------------------------------------------------ +class ExerciseUpdate(BaseModel): + beschreibung: Optional[str] = None + schritte: Optional[str] = None # JSON-String: '["Schritt 1", ...]' + tipp: Optional[str] = None + +@router.put("/exercises/{exercise_id}") +async def update_exercise(exercise_id: int, body: ExerciseUpdate, _=Depends(require_admin)): + """Partial update von beschreibung/schritte/tipp einer Übung (nur Admin).""" + with db() as conn: + row = conn.execute( + "SELECT id FROM training_exercises WHERE id=?", (exercise_id,) + ).fetchone() + if not row: + raise HTTPException(status_code=404, detail="Übung nicht gefunden") + fields, vals = [], [] + if body.beschreibung is not None: + fields.append("beschreibung=?"); vals.append(body.beschreibung) + if body.schritte is not None: + fields.append("schritte=?"); vals.append(body.schritte) + if body.tipp is not None: + fields.append("tipp=?"); vals.append(body.tipp) + if not fields: + return {"ok": True, "updated": 0} + vals.append(exercise_id) + conn.execute(f"UPDATE training_exercises SET {', '.join(fields)} WHERE id=?", vals) + return {"ok": True, "updated": len(fields)} + # ------------------------------------------------------------------ # Übungs-Status # ------------------------------------------------------------------ diff --git a/backend/static/index.html b/backend/static/index.html index 242e2b7..9fb7cef 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -409,6 +409,10 @@
++ Keine Ergebnisse +
++ ${_search + ? `Zu "${_esc(_search)}" wurde nichts gefunden.` + : 'Noch keine FAQ-Artikel vorhanden.'} +
+