Files
2026-10-30 22:47:42 +01:00

95 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
- [x] Phase 1: Docker setup, DB, auth with invitations, roles
- [x] Phase 2: Recipes CRUD, visibility (private/public), photos, notes, search, favourites
- [x] Phase 3: Comments, serving calculator (server-side `?portionen=`), cooking mode (wake lock)
- [x] Phase 4: AI (OpenAI): "Mit KI aufbereiten" on the form + photo/screenshot and text import
- [x] Phase 5: Link import (schema.org JSON-LD, Instagram best effort + paste/screenshot fallback, PWA share target)
- [x] 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
```bash
# 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),
stack in `/opt/stacks/kochbuch` (managed by the owner's Docker manager; dir owned by claude),
`.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 /opt/stacks/kochbuch && ./scripts/backup.sh && git pull && docker compose up -d --build`.
- Gitea: https://git.dammann.vip/dammann/Kochbuch (SSH alias `gitea`, port 2222).