95 lines
6.7 KiB
Markdown
95 lines
6.7 KiB
Markdown
# 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).
|