Runs backup.sh/restore.sh against a logging docker stub and checks archive content, retention, data flow, .env recovery and the order of stopping/starting the app. Co-Authored-By: Claude Opus 5.5 <[email protected]>
6.1 KiB
6.1 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 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.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).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.pyparses JSON-LD Recipe + page text (stdlib HTMLParser).instagram.pymakes 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 viascripts/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(volumemedia); 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:
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.