Files
Kochbuch/CLAUDE.md
T
2026-10-30 22:39:04 +01:00

6.6 KiB
Raw Blame History

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 best effort + paste/screenshot fallback, PWA share target)
  • Phase 6: Backup/restore scripts, 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.py for 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/ – custom User (e-mail login, role = user|admin), Invitation, PasswordResetToken, login/registration/reset views, admin area (admin_views.py, /verwaltung/…), ensure_admin management command (first admin from env).
  • recipes/ – Recipe (+ Ingredient, Step, Tag, RecipeImage, Favorite, Comment). RecipeQuerySet.visible_to/ editable_by/deletable_by/search hold all access rules. parsing.py converts the text form fields (one ingredient per line, Gruppe: headers, steps separated by blank lines) to structured data and scales amounts. images.py validates/re-encodes uploads (WebP 1600 px + 480 px thumb, no EXIF). Photos are served only via recipes: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.py lists every recipe URL – add new recipe routes there.
  • recipes/ai.py – OpenAI Chat Completions with strict json_schema (RECIPE_JSON_SCHEMA), answers re-validated by Pydantic (AIRecipe), map_api_error → German user messages, per-user/global daily limits (0 = off). _create_completion is the only network call (stubbed in tests). Drafts go into RecipeForm(initial=…) and are never saved directly. import_views.py: /rezepte/import/ (photo, text).
  • recipes/fetch.py – the ONLY way to request user-supplied URLs: resolves once, requires public IPs, connects to the checked IP (no DNS rebinding), manual redirects re-checked, size/time limits, honest UA. schema_org.py parses JSON-LD Recipe + page text (stdlib HTMLParser). instagram.py makes one honest request to the post's embed page; Instagram mostly answers with an empty shell → UI falls back to paste/screenshot. Do not add UA spoofing, cookies or login-based scraping (owner's requirement).
  • core/pwa.py – manifest (share_target → /teilen/) and a no-cache service worker. Icons via scripts/make_icons.py.
  • 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), backup.sh / restore.sh (pg_dump + media + .env in one archive, retention via KEEP), dev_server.py (local run without Docker), make_icons.py.
  • In the container MEDIA_ROOT=/data/media (volume media); never change without migrating files.

Commands

# Local (Windows dev box: venv lives at C:\Users\kyled\.venvs\kochbuch due to path-length issues)
python -m pytest                                  # tests (SQLite)
python -m ruff check .                            # lint (config in pyproject.toml)
sh tests/shell/test_backup_restore.sh             # backup/restore logic with a docker stub
# Postgres without Docker (dev box): pip install pgserver; start it, then
# POSTGRES_HOST=127.0.0.1 POSTGRES_PORT=<port> POSTGRES_USER=postgres POSTGRES_DB=postgres python -m pytest
# (the Windows pgserver build lacks tz data: copy tzdata/zoneinfo to pginstall/share/postgresql/timezone)
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: LoginRequiredMiddleware is active; public views need @login_not_required. Admin views use admin_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_KEY only via env.
  • Keep it lean: small server, no heavy dependencies without reason.

Deployment (owner's homelab)

  • Server srv-stackpilot (10.10.1.7, Debian 12, Docker 29), user claude (docker group, SSH key only), checkout in ~/kochbuch, .env there (mode 600, never commit). Port 8000 on the host.
  • Reverse proxy is NPMplus on the same host → proxy host kochbuch.dammann.vip → 10.10.1.7:8000.
  • Update: cd ~/kochbuch && ./scripts/backup.sh && git pull && docker compose up -d --build.
  • Gitea: https://git.dammann.vip/dammann/Kochbuch (SSH alias gitea, port 2222).