Backend: - DB: 3 neue Indizes (forum_posts thread+user, routes user) — Forum/Routen-Queries - Caching: cache.py (TTL-Cache ohne neue Dependency) für 5 statische Listen (training_exercises, pflege_tipps, wiki_stats, wiki_gruppen, help_articles) - diary.py + breeder_photos.py: Bildverarbeitung (ffmpeg/PIL/EXIF) per run_in_executor → blockiert Event-Loop nicht mehr - scheduler.py: 11 kollidierende Jobs auf 5-Min-Intervalle gestaggert, coalesce=True - social.py: ORDER BY RANDOM() ohne LIMIT in 2 Stellen gefixt - alerts.py: Haversine-Loop bekommt SQL-Bounding-Box-Vorfilter Frontend: - sw.js: Tile-Cache mit LRU-Eviction (max 500 Einträge) - admin.js: Event-Listener-Leak — Tab-Klicks per Delegation statt N Listener - api.js: compressImage() Helper — Client-seitiges Resize auf max 2000px (HEIC/Videos/<500KB unverändert), integriert in 8 Upload-Stellen (diary, dog-profile×2, walks, poison, lost, health×2) Bump APP_VER 1071 → 1072 (sw.js, app.js, main.py, index.html)
114 lines
4 KiB
Python
114 lines
4 KiB
Python
"""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
|
||
from cache import ttl_cache
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# Öffentliche, aktive FAQ-Liste – statisch, 1h TTL-Cache.
|
||
# Admin-Pfad (?all=1) wird NICHT gecached.
|
||
# Wird bei jedem schreibenden Admin-Endpoint unten invalidiert.
|
||
# ------------------------------------------------------------------
|
||
@ttl_cache(ttl=3600)
|
||
def _load_active_help_articles() -> list[dict]:
|
||
with db() as conn:
|
||
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]
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# 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
|
||
|
||
if show_all:
|
||
with db() as conn:
|
||
rows = conn.execute(
|
||
"SELECT id, kategorie, frage, antwort, sort_order, aktiv "
|
||
"FROM help_articles "
|
||
"ORDER BY kategorie, sort_order, id"
|
||
).fetchall()
|
||
return [dict(r) for r in rows]
|
||
|
||
# Öffentliche, aktive Artikel kommen aus dem Cache
|
||
return _load_active_help_articles()
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# 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),
|
||
)
|
||
_load_active_help_articles.cache_clear()
|
||
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),
|
||
)
|
||
_load_active_help_articles.cache_clear()
|
||
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,))
|
||
_load_active_help_articles.cache_clear()
|
||
return {"ok": True}
|