diff --git a/PROJEKT.md b/PROJEKT.md index 69ef35c..033b096 100644 --- a/PROJEKT.md +++ b/PROJEKT.md @@ -46,7 +46,7 @@ Maps: Leaflet.js + OpenStreetMap (kostenlos, kein Google-Lock) --- -## Implementierungsstand (aktuell: 2026-04-24, SW by-v356, APP_VER 343) +## Implementierungsstand (aktuell: 2026-04-24, SW by-v333, APP_VER 320) ### Fertig implementiert ✅ @@ -65,32 +65,6 @@ Maps: Leaflet.js + OpenStreetMap (kostenlos, kein Google-Lock) - Gesundheit, Admin, Karte-Legende: Tab-/Legende-Grid 2 Zeilen (gleiche CSS-Grid-Technik) - Hinweis: layout.css lädt vor components.css → ID-Selektor (#page-forum, #page-health, #page-admin, #page-map) nötig für Spezifität -#### Social Media Manager (2026-04-24) -- Neue Rolle `is_social_media` — eigene Seite `/social` -- Luna KI-Coach: Themen-Vorschläge, Fortschrittsbalken, rotierende Nachrichten -- **Rasse des Tages**: 1003 Wiki-Rassen = 2,75 Jahre täglicher Content mit Bild -- **🎾 Trainingstipp**: 104 Übungen in 7 Kategorien, 3 Stil-Varianten -- **🛁 Pflegetipp**: 43 Tipps rassenspezifisch, auch für normale User im Hundeprofil -- Diversitäts-Check (Warnung wenn Kategorie >40% dominiert) -- Post-Bestätigung mit Datum + URL, Ausstehend-Banner -- Medien-Upload (Kamera/Mediathek/Dateien), Instagram-Vorschau -- XP/Level-System (Rookie → 👑 Star) -- Admin: Social-Tracking (published/scheduled/ideas + letzte 10 Posts) - -#### Pflege-System (2026-04-24) -- `pflege_tipps` DB-Tabelle: 43 Tipps in 10 Kategorien (Fell, Krallen, Zähne, Ohren, Augen, Pfoten, Parasiten, Saisonal, Gesundheitsvorsorge, Welpen-Pflege) -- Hundeprofil: 🛁 Pflegetipps — Tipp des Tages (saisonal) + vollständige Kategorieliste -- Rassen-Autocomplete im Hundeprofil mit Wiki-Match-Badge -- `dogs.rasse_id` FK → `wiki_rassen` für präzise Filterung - -#### Breed-Enricher Wikipedia-grounded (2026-04-24) -- Korrektheit 2.3→~4.5 durch Wikipedia-Quelltext als Basis -- Claude Haiku extrahiert Fakten aus Wikipedia-Text (de/en Fallback) -- `ki_source` ('wikipedia_de/en'/'none'), `ki_model` getrackt -- LLM-as-Judge Evaluator im Admin, Gemma-Reset-Button -- 1003 Rassen, limit=2000 (ein Rutsch) -- LM Studio: Mac 10.47.11.70:11435, Modell gemma-4-31b-it - #### Infrastruktur - SSH-Port DS1621: 4711 (geändert von 22, 2026-04-24) diff --git a/backend/main.py b/backend/main.py index 4cfad18..2865269 100644 --- a/backend/main.py +++ b/backend/main.py @@ -121,7 +121,6 @@ from routes.training import router as training_router from routes.praise import router as praise_router from routes.weather import router as weather_router from routes.social import router as social_router -from routes.moderation import router as moderation_router app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) @@ -162,7 +161,6 @@ app.include_router(stats_router, prefix="/api/stats", tags=[ app.include_router(achievements_router, prefix="/api/achievements", tags=["Achievements"]) app.include_router(training_router, prefix="/api/training", tags=["Training"]) app.include_router(praise_router, prefix="/api/praise", tags=["Praise"]) -app.include_router(moderation_router, prefix="/api/moderation", tags=["Moderation"]) # ------------------------------------------------------------------ diff --git a/backend/routes/moderation.py b/backend/routes/moderation.py deleted file mode 100644 index 128748b..0000000 --- a/backend/routes/moderation.py +++ /dev/null @@ -1,223 +0,0 @@ -"""BAN YARO — Moderations-Panel Backend""" -from fastapi import APIRouter, Depends, HTTPException -from database import db -from auth import get_current_user - -router = APIRouter() - - -# ------------------------------------------------------------------ -# Dependency: Moderator oder Admin -# ------------------------------------------------------------------ -def require_moderator(user=Depends(get_current_user)): - if not (user.get("is_moderator") or user["rolle"] == "admin"): - raise HTTPException(403, "Nur für Moderatoren.") - return user - - -# ------------------------------------------------------------------ -# GET /api/moderation/stats — Übersicht -# ------------------------------------------------------------------ -@router.get("/stats") -async def mod_stats(user=Depends(require_moderator)): - with db() as conn: - open_reports = conn.execute( - "SELECT COUNT(*) FROM forum_reports WHERE resolved=0" - ).fetchone()[0] - - pending_fotos = 0 - try: - pending_fotos = conn.execute( - "SELECT COUNT(*) FROM wiki_foto_submissions WHERE status='pending'" - ).fetchone()[0] - except Exception: - pass - - banned_users = conn.execute( - "SELECT COUNT(*) FROM users WHERE is_banned=1" - ).fetchone()[0] - - pending_zuchter = 0 - try: - pending_zuchter = conn.execute( - "SELECT COUNT(*) FROM wiki_zuchter WHERE verified=0" - ).fetchone()[0] - except Exception: - pass - - return { - "open_reports": open_reports, - "pending_fotos": pending_fotos, - "banned_users": banned_users, - "pending_zuchter": pending_zuchter, - } - - -# ------------------------------------------------------------------ -# GET /api/moderation/reports — gemeldete Inhalte -# ------------------------------------------------------------------ -@router.get("/reports") -async def mod_reports(user=Depends(require_moderator)): - with db() as conn: - rows = conn.execute(""" - SELECT r.id, r.target_type, r.target_id, r.grund, r.resolved, r.created_at, - u.name AS melder_name, - CASE r.target_type - WHEN 'thread' THEN (SELECT t.titel FROM forum_threads t WHERE t.id=r.target_id) - WHEN 'post' THEN (SELECT SUBSTR(p.text,1,80) FROM forum_posts p WHERE p.id=r.target_id) - END AS content_preview - FROM forum_reports r - LEFT JOIN users u ON u.id=r.user_id - WHERE r.resolved=0 - ORDER BY r.created_at DESC - LIMIT 100 - """).fetchall() - return [dict(r) for r in rows] - - -# ------------------------------------------------------------------ -# PATCH /api/moderation/reports/{id} — Meldung erledigen -# ------------------------------------------------------------------ -@router.patch("/reports/{rid}") -async def mod_resolve_report(rid: int, user=Depends(require_moderator)): - with db() as conn: - r = conn.execute( - "SELECT resolved FROM forum_reports WHERE id=?", (rid,) - ).fetchone() - if not r: - raise HTTPException(404, "Meldung nicht gefunden.") - new_state = 0 if r["resolved"] else 1 - conn.execute( - "UPDATE forum_reports SET resolved=? WHERE id=?", - (new_state, rid) - ) - return {"ok": True} - - -# ------------------------------------------------------------------ -# GET /api/moderation/users — User-Liste (Basisinfos) -# ------------------------------------------------------------------ -@router.get("/users") -async def mod_users( - q: str = "", - banned: int = 0, - limit: int = 50, - offset: int = 0, - user=Depends(require_moderator), -): - with db() as conn: - where = "WHERE 1=1" - params = [] - if q.strip(): - where += " AND (name LIKE ? OR email LIKE ?)" - params.extend([f"%{q.strip()}%", f"%{q.strip()}%"]) - if banned: - where += " AND is_banned=1" - - # E-Mail nur für Admins; Moderatoren sehen maskierte Version - email_col = "email" if user["rolle"] == "admin" else \ - "SUBSTR(email,1,2)||'***@'||SUBSTR(email,INSTR(email,'@')+1) AS email" - - rows = conn.execute(f""" - SELECT id, name, {email_col}, rolle, is_moderator, is_banned, ban_reason, created_at - FROM users - {where} - ORDER BY created_at DESC - LIMIT ? OFFSET ? - """, [*params, limit, offset]).fetchall() - - total = conn.execute( - f"SELECT COUNT(*) FROM users {where}", params - ).fetchone()[0] - - return {"users": [dict(r) for r in rows], "total": total} - - -# ------------------------------------------------------------------ -# PATCH /api/moderation/users/{id} — Ban / Unban -# ------------------------------------------------------------------ -@router.patch("/users/{uid}") -async def mod_patch_user(uid: int, data: dict, user=Depends(require_moderator)): - allowed_fields = {"is_banned", "ban_reason"} - updates = {k: v for k, v in data.items() if k in allowed_fields} - if not updates: - raise HTTPException(400, "Keine erlaubten Felder.") - - with db() as conn: - target = conn.execute( - "SELECT id, rolle, name FROM users WHERE id=?", (uid,) - ).fetchone() - if not target: - raise HTTPException(404, "User nicht gefunden.") - if target["rolle"] == "admin" and user["rolle"] != "admin": - raise HTTPException(403, "Admins können nur von Admins verwaltet werden.") - - cols = ", ".join(f"{k}=?" for k in updates) - conn.execute( - f"UPDATE users SET {cols} WHERE id=?", - [*updates.values(), uid] - ) - row = conn.execute( - "SELECT id, name, rolle, is_banned, ban_reason FROM users WHERE id=?", - (uid,) - ).fetchone() - return dict(row) - - -# ------------------------------------------------------------------ -# GET /api/moderation/fotos — Wiki-Foto-Einreichungen (pending) -# ------------------------------------------------------------------ -@router.get("/fotos") -async def mod_fotos(user=Depends(require_moderator)): - with db() as conn: - try: - rows = conn.execute(""" - SELECT s.id, s.rasse_slug, s.foto_url, s.created_at, - u.name AS user_name, - r.name AS rasse_name, r.foto_url AS aktuell_foto - FROM wiki_foto_submissions s - LEFT JOIN users u ON u.id=s.user_id - LEFT JOIN wiki_rassen r ON r.slug=s.rasse_slug - WHERE s.status='pending' - ORDER BY s.created_at ASC - LIMIT 50 - """).fetchall() - return [dict(r) for r in rows] - except Exception: - return [] - - -# ------------------------------------------------------------------ -# PATCH /api/moderation/fotos/{id} — Foto genehmigen / ablehnen -# ------------------------------------------------------------------ -@router.patch("/fotos/{foto_id}") -async def mod_foto_action(foto_id: int, data: dict, user=Depends(require_moderator)): - action = data.get("action") - if action not in ("approve", "reject"): - raise HTTPException(400, "action muss 'approve' oder 'reject' sein.") - - with db() as conn: - sub = conn.execute( - "SELECT id, rasse_slug, foto_url FROM wiki_foto_submissions WHERE id=?", - (foto_id,) - ).fetchone() - if not sub: - raise HTTPException(404, "Einreichung nicht gefunden.") - - if action == "approve": - conn.execute( - "UPDATE wiki_foto_submissions SET status='approved' WHERE id=?", - (foto_id,) - ) - conn.execute( - "UPDATE wiki_rassen SET foto_url=? WHERE slug=?", - (sub["foto_url"], sub["rasse_slug"]) - ) - else: - reason = data.get("reject_reason", "Nicht geeignet.") - conn.execute( - "UPDATE wiki_foto_submissions SET status='rejected', reject_reason=? WHERE id=?", - (reason, foto_id) - ) - - return {"ok": True} diff --git a/backend/static/index.html b/backend/static/index.html index 14b694d..ba0cd02 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -185,11 +185,6 @@ Social Media - -