- recipes/ai.py: Chat Completions with strict JSON schema (Structured Outputs), Pydantic re-validation, plausibility clamping, prompt treats input as data only - "Mit KI aufbereiten" on create/edit form returns an editable draft - Import page: photo/screenshot (vision, downscaled JPEG, not stored) and pasted text; drafts pre-fill the recipe form with a preview - Clear UI messages for timeout, rate limit, quota, auth, model and connection errors; without OPENAI_API_KEY AI features show a notice - Optional per-user and global daily AI limits (default unlimited) - Tests for response parsing, schema strictness, error mapping, views Co-Authored-By: Claude Opus 5.5 <[email protected]>
4.8 KiB
4.8 KiB
Kochbuch – Notes for Claude
Self-hosted recipe web app for a homelab. Runs via docker compose behind the owner's existing Nginx + Cloudflare (public URL: https://kochbuch.dammann.vip). The app itself serves plain HTTP on one port; TLS, domain and proxy are out of scope.
Status
- Phase 1: Docker setup, DB, auth with invitations, roles
- Phase 2: Recipes CRUD, visibility (private/public), photos, notes, search, favourites
- Phase 3: Comments, serving calculator (server-side
?portionen=), cooking mode (wake lock) - Phase 4: AI (OpenAI): "Mit KI aufbereiten" on the form + photo/screenshot and text import
- Phase 5: Link import (schema.org JSON-LD, Instagram with paste fallback, Android PWA share target)
- Phase 6: Backup script, full README
Stack
Django 5.2 LTS (Python 3.12), server-rendered templates, PostgreSQL 17,
gunicorn (gthread), WhiteNoise for static files, Pillow + pillow-heif for photos. No Redis, no HTMX,
no JS build step: plain forms with small progressive enhancements in static/js/app.js.
Layout
config/– settings (all env-driven, see.env.example), urls, wsgi.settings_test.pyfor pytest.core/– cross-cutting:ProxyMiddleware(trusted proxies, client IP),SecurityHeadersMiddleware(CSP, Permissions-Policy, no-store for logged-in pages), DB-backed rate limiter (core/ratelimit.py),/healthz, error pages.accounts/– customUser(e-mail login,role= user|admin),Invitation,PasswordResetToken, login/registration/reset views, admin area (admin_views.py,/verwaltung/…),ensure_adminmanagement command (first admin from env).recipes/–Recipe(+Ingredient,Step,Tag,RecipeImage,Favorite,Comment).RecipeQuerySet.visible_to/ editable_by/deletable_by/searchhold all access rules.parsing.pyconverts the text form fields (one ingredient per line,Gruppe:headers, steps separated by blank lines) to structured data and scales amounts.images.pyvalidates/re-encodes uploads (WebP 1600 px + 480 px thumb, no EXIF). Photos are served only viarecipes:photo(/bilder/<uuid>/<gross|klein>.webp) after a visibility check. Comments: deletable by their author and admins only (CommentQuerySet.deletable_by), rate-limited.tests/test_privacy.pylists every recipe URL – add new recipe routes there.recipes/ai.py– OpenAI Chat Completions with strictjson_schema(RECIPE_JSON_SCHEMA), answers re-validated by Pydantic (AIRecipe),map_api_error→ German user messages, per-user/global daily limits (0 = off)._create_completionis the only network call (stubbed in tests). Drafts go intoRecipeForm(initial=…)and are never saved directly.import_views.py:/rezepte/import/(photo, text).templates/,static/– global templates,app.css(mobile first, dark mode),app.js.docker/entrypoint.sh– migrate → ensure_admin → purge_ratelimits → gunicorn.scripts/–init-env.sh(creates .env with secrets),dev_server.py(local run without Docker).
Commands
# Local (Windows dev box: venv lives at C:\Users\kyled\.venvs\kochbuch due to path-length issues)
python -m pytest # tests (SQLite)
python scripts/dev_server.py # http://localhost:8000, [email protected]
SECRET_KEY=dev python manage.py makemigrations
# Docker
./scripts/init-env.sh [email protected] # once
docker compose up -d --build
docker compose --profile test run --rm test # tests against Postgres
docker compose logs -f app
Conventions
- Code, comments, commits: English. UI, README, user-facing messages: German (informal "du").
- Default deny:
LoginRequiredMiddlewareis active; public views need@login_not_required. Admin views useadmin_required(403). Private content that a user may not see → 404, never 403 (don't leak existence). - Visibility (Phase 2+): every recipe query for a user goes through one central queryset method
(
visible_to(user)), including list, detail, search, tags, images, comments. Uploaded files are never served from a static path – only through permission-checked views. Tests must cover each access path. - Tags are stored lowercase.
- One-time tokens (invitations, resets) are stored only as SHA-256 hashes; the link is shown once.
- Rate limits use
core.ratelimit.hit/is_limited; limit 0 = unlimited. AI limits (Phase 4) must exist but default to 0 (owner's request), configurable via env. - Forwarded headers are trusted only from
TRUSTED_PROXIES; gunicorn does not trust any itself. - CSP is strict (
script-src 'self',style-src 'self'): no inline scripts/styles/handlers in templates. - AI output is always an editable draft, never saved directly.
OPENAI_API_KEYonly via env. - Keep it lean: small server, no heavy dependencies without reason.