Feature: Hundeernährungs-Feature — Kalorien-Rechner, Futter-Guide, Giftliste, KI-Berater (SW by-v698)

This commit is contained in:
rene 2026-05-04 20:51:45 +02:00
parent b1d9fb4f54
commit 6e4bf25581
7 changed files with 838 additions and 8 deletions

View file

@ -6,9 +6,10 @@ import os
import html
import logging
from collections import deque
import httpx
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse
from fastapi.responses import FileResponse, JSONResponse, Response
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from brotli_asgi import BrotliMiddleware
@ -43,10 +44,43 @@ logger = logging.getLogger(__name__)
# ------------------------------------------------------------------
# Startup / Shutdown
# ------------------------------------------------------------------
def _backfill_image_sizes():
"""Füllt img_width/img_height für alle diary_media-Bilder ohne Maße nach."""
import io
from database import db
from media_utils import get_image_size
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
with db() as conn:
rows = conn.execute(
"SELECT id, url FROM diary_media WHERE media_type='image' AND img_width IS NULL"
).fetchall()
if not rows:
return
logger.info("Backfill Bildmaße: %d Einträge...", len(rows))
updated = 0
for row in rows:
# url ist z.B. /media/diary/xxx.jpg → Pfad: MEDIA_DIR/diary/xxx.jpg
rel = row["url"].removeprefix("/media/")
path = os.path.join(MEDIA_DIR, rel)
try:
with open(path, "rb") as f:
data = f.read()
size = get_image_size(data)
if size:
with db() as conn:
conn.execute(
"UPDATE diary_media SET img_width=?, img_height=? WHERE id=?",
(size[0], size[1], row["id"])
)
updated += 1
except Exception:
pass
logger.info("Backfill Bildmaße abgeschlossen: %d/%d aktualisiert.", updated, len(rows))
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("Ban Yaro startet...")
init_db()
_backfill_image_sizes()
from routes.movies import seed_movies
seed_movies()
logger.info(f"KI-Modus: {ki.KI_MODE}")
@ -76,7 +110,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://umami.motocamp.de; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: blob: https:; "
"connect-src 'self' https:; "
@ -198,6 +232,7 @@ from routes.adoption import router as adoption_router
from routes.health_docs import router as health_docs_router
from routes.passport import router as passport_router
from routes.playdate import router as playdate_router
from routes.ernaehrung import router as ernaehrung_router
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
@ -256,6 +291,7 @@ app.include_router(adoption_router, prefix="/api/adoption", ta
app.include_router(health_docs_router, prefix="/api/health-docs", tags=["Gesundheitsdokumente"])
app.include_router(passport_router, prefix="/api/passport", tags=["Hundepass"])
app.include_router(playdate_router, prefix="/api/playdate", tags=["Playdate"])
app.include_router(ernaehrung_router, prefix="/api/dogs", tags=["Ernährung"])
# ------------------------------------------------------------------
@ -285,6 +321,27 @@ 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.get("/stats/script.js")
async def umami_script_proxy():
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get("https://umami.motocamp.de/script.js")
return Response(content=r.content, media_type="application/javascript",
headers={"Cache-Control": "public, max-age=86400"})
@app.post("/stats/api/send")
async def umami_send_proxy(request: Request):
body = await request.body()
async with httpx.AsyncClient(timeout=10) as client:
r = await client.post(
"https://umami.motocamp.de/api/send",
content=body,
headers={"Content-Type": "application/json",
"User-Agent": request.headers.get("user-agent", "")},
)
return Response(content=r.content, status_code=r.status_code,
media_type="application/json")
@app.get("/robots.txt")
async def robots():
return FileResponse(f"{STATIC_DIR}/robots.txt", media_type="text/plain")