Session 2026-04-21: SEO, Wiki-Anreicherung, Training, Lober
SEO & Crawler:
- robots.txt, llms.txt, sitemap.xml (508 Seiten bei Google)
- SSR-Seiten: /info, /wiki/rassen, /wiki/rasse/{slug}, /knigge
- Open Graph, JSON-LD, Breadcrumbs in index.html
Navigation:
- Training unter "Mein Hund", Wissen collapsible
- Welcome-Seite und Landing-Page auf 5-Gruppen-Struktur
Wiki:
- KI-Anreicherung (Claude API): beschreibung, vorkommen_de, Steckbrief
- "So einen hab ich" / Züchter-Verzeichnis
- Scheduler: 50 Rassen beim Start, 20/Nacht
Training:
- Session-Logging (Erfolgsquote, Stimmung, Zufriedenheit)
- Virtueller KI-Trainer (6h-Cache)
- Trainingskalender (Habit-Tracker)
- Top-Training → automatischer Tagebucheintrag
- Gamification ohne Druck: Badges, Streak, Stats
Fortschritts-Lober:
- Jeden Montag 09:00: Claude schreibt Lob-Text pro Hund
- Push + Karte im Tagebuch
Monitoring:
- 4× täglich Status-Mail mit Scheduler-Status + Wiki-Fortschritt
This commit is contained in:
parent
65d1cf6c7f
commit
180de32e57
22 changed files with 4351 additions and 189 deletions
|
|
@ -7,7 +7,7 @@ import logging
|
|||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
||||
from pydantic import BaseModel
|
||||
from database import db
|
||||
from auth import get_current_user
|
||||
from auth import get_current_user, get_current_user_optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
|
|
@ -437,3 +437,241 @@ async def review_submission(sub_id: int, data: ReviewModel, user=Depends(get_cur
|
|||
pass
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hilfsfunktion: Stats für eine Rasse zusammenstellen
|
||||
# ------------------------------------------------------------------
|
||||
def _build_stats(conn, rasse_slug: str, user_id=None) -> dict:
|
||||
dogs_count = conn.execute(
|
||||
"SELECT COUNT(DISTINCT user_id) FROM dogs WHERE LOWER(rasse) = LOWER(?)",
|
||||
(rasse_slug,),
|
||||
).fetchone()[0]
|
||||
|
||||
hat_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_breed_interest WHERE rasse_slug=? AND typ='hat'",
|
||||
(rasse_slug,),
|
||||
).fetchone()[0]
|
||||
|
||||
will_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_breed_interest WHERE rasse_slug=? AND typ='will'",
|
||||
(rasse_slug,),
|
||||
).fetchone()[0]
|
||||
|
||||
zuchter_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_zuchter WHERE rasse_slug=? AND verified=1",
|
||||
(rasse_slug,),
|
||||
).fetchone()[0]
|
||||
|
||||
berichte_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_berichte WHERE rasse=?",
|
||||
(rasse_slug,),
|
||||
).fetchone()[0]
|
||||
|
||||
user_interest = None
|
||||
if user_id:
|
||||
row = conn.execute(
|
||||
"SELECT typ FROM wiki_breed_interest WHERE user_id=? AND rasse_slug=?",
|
||||
(user_id, rasse_slug),
|
||||
).fetchone()
|
||||
if row:
|
||||
user_interest = row["typ"]
|
||||
|
||||
return {
|
||||
"dogs_count": dogs_count,
|
||||
"hat_count": hat_count,
|
||||
"will_count": will_count,
|
||||
"zuchter_count": zuchter_count,
|
||||
"berichte_count":berichte_count,
|
||||
"user_interest": user_interest,
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/wiki/rassen/{slug}/stats
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/rassen/{slug}/stats")
|
||||
async def get_rasse_stats(slug: str, user=Depends(get_current_user_optional)):
|
||||
with db() as conn:
|
||||
rasse = conn.execute(
|
||||
"SELECT slug FROM wiki_rassen WHERE slug=?", (slug,)
|
||||
).fetchone()
|
||||
if not rasse:
|
||||
raise HTTPException(404, "Rasse nicht gefunden.")
|
||||
return _build_stats(conn, slug, user["id"] if user else None)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schemas für Interesse und Züchter
|
||||
# ------------------------------------------------------------------
|
||||
class InteresseCreate(BaseModel):
|
||||
typ: str # "hat" oder "will"
|
||||
|
||||
class ZuchterCreate(BaseModel):
|
||||
rasse_slug: str
|
||||
name: str
|
||||
zwingername: str = ""
|
||||
ort: str = ""
|
||||
plz: str = ""
|
||||
bundesland: str = ""
|
||||
vdh_mitglied: int = 0
|
||||
website: str = ""
|
||||
telefon: str = ""
|
||||
beschreibung: str = ""
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/wiki/rassen/{slug}/interesse
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/rassen/{slug}/interesse")
|
||||
async def set_interesse(slug: str, data: InteresseCreate, user=Depends(get_current_user)):
|
||||
if data.typ not in ("hat", "will"):
|
||||
raise HTTPException(400, "typ muss 'hat' oder 'will' sein.")
|
||||
with db() as conn:
|
||||
rasse = conn.execute(
|
||||
"SELECT slug FROM wiki_rassen WHERE slug=?", (slug,)
|
||||
).fetchone()
|
||||
if not rasse:
|
||||
raise HTTPException(404, "Rasse nicht gefunden.")
|
||||
conn.execute(
|
||||
"""INSERT INTO wiki_breed_interest (user_id, rasse_slug, typ)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id, rasse_slug) DO UPDATE SET typ=excluded.typ""",
|
||||
(user["id"], slug, data.typ),
|
||||
)
|
||||
return _build_stats(conn, slug, user["id"])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /api/wiki/rassen/{slug}/interesse
|
||||
# ------------------------------------------------------------------
|
||||
@router.delete("/rassen/{slug}/interesse")
|
||||
async def delete_interesse(slug: str, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"DELETE FROM wiki_breed_interest WHERE user_id=? AND rasse_slug=?",
|
||||
(user["id"], slug),
|
||||
)
|
||||
return _build_stats(conn, slug, user["id"])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/wiki/rassen/{slug}/zuchter
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/rassen/{slug}/zuchter")
|
||||
async def get_zuchter_fuer_rasse(
|
||||
slug: str,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
with db() as conn:
|
||||
rows = conn.execute(
|
||||
"""SELECT z.id, z.rasse_slug, z.name, z.zwingername, z.ort, z.plz,
|
||||
z.bundesland, z.vdh_mitglied, z.website, z.telefon,
|
||||
z.beschreibung, z.created_at
|
||||
FROM wiki_zuchter z
|
||||
WHERE z.rasse_slug=? AND z.verified=1
|
||||
ORDER BY z.bundesland ASC, z.ort ASC
|
||||
LIMIT ? OFFSET ?""",
|
||||
(slug, limit, offset),
|
||||
).fetchall()
|
||||
total = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_zuchter WHERE rasse_slug=? AND verified=1",
|
||||
(slug,),
|
||||
).fetchone()[0]
|
||||
return {"zuchter": [dict(r) for r in rows], "total": total}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/wiki/zuchter — Züchter einreichen
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/zuchter", status_code=201)
|
||||
async def create_zuchter(data: ZuchterCreate, user=Depends(get_current_user)):
|
||||
if not data.name.strip():
|
||||
raise HTTPException(400, "Name darf nicht leer sein.")
|
||||
with db() as conn:
|
||||
rasse = conn.execute(
|
||||
"SELECT slug FROM wiki_rassen WHERE slug=?", (data.rasse_slug,)
|
||||
).fetchone()
|
||||
if not rasse:
|
||||
raise HTTPException(400, "Ungültige Rasse.")
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO wiki_zuchter
|
||||
(rasse_slug, name, zwingername, ort, plz, bundesland,
|
||||
vdh_mitglied, website, telefon, beschreibung, verified, user_id)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,0,?)""",
|
||||
(
|
||||
data.rasse_slug, data.name.strip(),
|
||||
data.zwingername.strip() or None,
|
||||
data.ort.strip() or None,
|
||||
data.plz.strip() or None,
|
||||
data.bundesland.strip() or None,
|
||||
data.vdh_mitglied,
|
||||
data.website.strip() or None,
|
||||
data.telefon.strip() or None,
|
||||
data.beschreibung.strip() or None,
|
||||
user["id"],
|
||||
),
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM wiki_zuchter WHERE id=?", (cur.lastrowid,)
|
||||
).fetchone()
|
||||
return dict(row)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /api/wiki/zuchter/{id} — eigene Einreichung löschen
|
||||
# ------------------------------------------------------------------
|
||||
@router.delete("/zuchter/{zuchter_id}")
|
||||
async def delete_zuchter(zuchter_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id, user_id FROM wiki_zuchter WHERE id=?", (zuchter_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Züchter nicht gefunden.")
|
||||
is_admin = user.get("rolle") == "admin" or user.get("is_moderator")
|
||||
if row["user_id"] != user["id"] and not is_admin:
|
||||
raise HTTPException(403, "Nicht erlaubt.")
|
||||
conn.execute("DELETE FROM wiki_zuchter WHERE id=?", (zuchter_id,))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/wiki/zuchter/pending — unverifizierte Einreichungen (Mod/Admin)
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/zuchter/pending")
|
||||
async def list_zuchter_pending(user=Depends(get_current_user)):
|
||||
if not (user.get("is_moderator") or user.get("rolle") in ("admin", "moderator")):
|
||||
raise HTTPException(403, "Nur Moderatoren.")
|
||||
with db() as conn:
|
||||
rows = conn.execute(
|
||||
"""SELECT z.*, u.name AS user_name
|
||||
FROM wiki_zuchter z
|
||||
LEFT JOIN users u ON u.id = z.user_id
|
||||
WHERE z.verified=0
|
||||
ORDER BY z.created_at ASC""",
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PATCH /api/wiki/zuchter/{id}/verify — Züchter freigeben (Mod/Admin)
|
||||
# ------------------------------------------------------------------
|
||||
@router.patch("/zuchter/{zuchter_id}/verify")
|
||||
async def verify_zuchter(zuchter_id: int, user=Depends(get_current_user)):
|
||||
if not (user.get("is_moderator") or user.get("rolle") in ("admin", "moderator")):
|
||||
raise HTTPException(403, "Nur Moderatoren.")
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id FROM wiki_zuchter WHERE id=?", (zuchter_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Züchter nicht gefunden.")
|
||||
conn.execute(
|
||||
"UPDATE wiki_zuchter SET verified=1 WHERE id=?", (zuchter_id,)
|
||||
)
|
||||
result = conn.execute(
|
||||
"SELECT * FROM wiki_zuchter WHERE id=?", (zuchter_id,)
|
||||
).fetchone()
|
||||
return dict(result)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue