diff --git a/backend/database.py b/backend/database.py index 232c503..c1b3f9f 100644 --- a/backend/database.py +++ b/backend/database.py @@ -26,6 +26,9 @@ def get_connection() -> sqlite3.Connection: conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") conn.execute("PRAGMA busy_timeout=5000") + conn.execute("PRAGMA cache_size = -32000") # 32MB Page-Cache + conn.execute("PRAGMA temp_store = MEMORY") # Temp-Tabellen im RAM statt auf Disk + conn.execute("PRAGMA mmap_size = 268435456") # 256MB Memory-Mapped I/O conn.create_function('norm', 1, _norm) return conn @@ -1212,3 +1215,11 @@ def _migrate(conn_factory): ) """) logger.info("Migration: dogs.gewicht_kg aus health synchronisiert.") + + # Performance-Indizes für häufige Queries + conn.execute("CREATE INDEX IF NOT EXISTS idx_health_naechstes ON health(naechstes) WHERE naechstes IS NOT NULL") + conn.execute("CREATE INDEX IF NOT EXISTS idx_ki_calls_user_date ON ki_daily_calls(user_id, date)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_events_user_datum ON events(user_id, datum)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_diary_created ON diary(dog_id, created_at DESC)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(user_id, created_at DESC)") + logger.info("Migration: Performance-Indizes bereit.") diff --git a/backend/main.py b/backend/main.py index d9e0d71..2bf6b4a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -10,6 +10,7 @@ from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, JSONResponse from starlette.middleware.base import BaseHTTPMiddleware +from fastapi.middleware.gzip import GZipMiddleware from contextlib import asynccontextmanager from database import init_db @@ -98,6 +99,20 @@ class _CacheControlMiddleware(BaseHTTPMiddleware): app.add_middleware(_CacheControlMiddleware) +class MediaCacheMiddleware(BaseHTTPMiddleware): + """Setzt aggressive Cache-Header für /media/-Requests. + UUID-basierte Dateinamen ändern sich nie → immutable caching. + """ + async def dispatch(self, request: Request, call_next): + response = await call_next(request) + if request.url.path.startswith('/media/'): + response.headers['Cache-Control'] = 'public, max-age=31536000, immutable' + return response + +app.add_middleware(MediaCacheMiddleware) +app.add_middleware(GZipMiddleware, minimum_size=1000) + + # ------------------------------------------------------------------ # API-Router registrieren (werden nach und nach hinzugefügt) # ------------------------------------------------------------------ diff --git a/backend/media_utils.py b/backend/media_utils.py index 10d9ec1..8a8698f 100644 --- a/backend/media_utils.py +++ b/backend/media_utils.py @@ -162,7 +162,7 @@ _PREVIEW_MAX = 800 # längste Seite in Pixeln def generate_preview(data: bytes, ext: str) -> bytes | None: - """Erzeugt ein JPEG-Preview (max _PREVIEW_MAX px). Gibt None zurück bei Videos/PDFs/Fehler.""" + """Erzeugt ein WebP-Preview (max _PREVIEW_MAX px). Gibt None zurück bei Videos/PDFs/Fehler.""" if ext.lower() not in _PREVIEW_EXTS: return None try: @@ -172,7 +172,7 @@ def generate_preview(data: bytes, ext: str) -> bytes | None: img = img.convert("RGB") img.thumbnail((_PREVIEW_MAX, _PREVIEW_MAX), Image.LANCZOS) buf = io.BytesIO() - img.save(buf, format="JPEG", quality=72, optimize=True) + img.save(buf, format="WEBP", quality=80) return buf.getvalue() except Exception: return None @@ -185,11 +185,11 @@ def preview_url_from(url: str | None) -> str | None: return None low = url.lower() if any(low.endswith(s) for s in ( - ".mp4", ".webm", ".mov", ".avi", ".pdf", "_thumb.jpg", "_preview.jpg" + ".mp4", ".webm", ".mov", ".avi", ".pdf", "_thumb.jpg", "_preview.jpg", "_preview.webp" )): return None base, _ = os.path.splitext(url) - return base + "_preview.jpg" + return base + "_preview.webp" def extract_video_thumb(video_path: str) -> str | None: diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 46448e1..facd4d2 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -829,7 +829,7 @@ async def generate_media_previews(user=Depends(require_admin)): base, ext = os.path.splitext(fname) if ext.lower() not in _PREVIEW_EXTS: continue - preview_path = os.path.join(folder, base + "_preview.jpg") + preview_path = os.path.join(folder, base + "_preview.webp") if os.path.exists(preview_path): skipped += 1 continue diff --git a/backend/routes/diary.py b/backend/routes/diary.py index 6610d81..a3dee2b 100644 --- a/backend/routes/diary.py +++ b/backend/routes/diary.py @@ -686,7 +686,7 @@ async def upload_media(dog_id: int, entry_id: int, elif media_type == "image": preview_bytes = generate_preview(raw_data, ext) if preview_bytes: - preview_path = os.path.splitext(path)[0] + "_preview.jpg" + preview_path = os.path.splitext(path)[0] + "_preview.webp" with open(preview_path, "wb") as f: f.write(preview_bytes) diff --git a/backend/routes/forum.py b/backend/routes/forum.py index 18f8102..d22a460 100644 --- a/backend/routes/forum.py +++ b/backend/routes/forum.py @@ -90,7 +90,7 @@ def _save_upload(file: UploadFile, data: bytes) -> str: else: preview_bytes = generate_preview(data, ext) if preview_bytes: - with open(os.path.splitext(path)[0] + "_preview.jpg", "wb") as f: + with open(os.path.splitext(path)[0] + "_preview.webp", "wb") as f: f.write(preview_bytes) return f"/media/forum/{filename}" diff --git a/backend/static/js/app.js b/backend/static/js/app.js index e6c8974..187591e 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '416'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '417'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index 076653f..a39eccd 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -598,7 +598,9 @@ window.Page_diary = (() => { content.innerHTML = `