Add phase 5: link import, Instagram fallback and share target

- SSRF-safe fetcher: public IPs only, connects to the checked address,
  redirects re-validated, size/time limits, honest user agent
- schema.org/Recipe JSON-LD extraction (graphs, HowToSections, ISO
  durations, yields, keywords, images) without extra dependencies;
  AI fallback on plain page text when no structured recipe exists
- Instagram: one login-free request to the public embed page; when
  Instagram withholds the caption, the import page explains how to paste
  the text or import a screenshot instead
- Suggested source photo can be downloaded and attached on save
- Text import works without AI (text becomes the description)
- Installable PWA with manifest, icons and Android share target
- Offline tests for SSRF rules, parsers, Instagram and import views

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Kyle Dammann
2026-10-30 21:53:28 +01:00
co-authored by Claude Opus 5.5
parent 1bf8ef8b65
commit 60785e2437
22 changed files with 1081 additions and 16 deletions
+8 -1
View File
@@ -9,7 +9,7 @@ port; TLS, domain and proxy are out of scope.
- [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
- [ ] Phase 5: Link import (schema.org JSON-LD, Instagram with paste fallback, Android PWA share target)
- [x] Phase 5: Link import (schema.org JSON-LD, Instagram best effort + paste/screenshot fallback, PWA share target)
- [ ] Phase 6: Backup script, full README
## Stack
@@ -36,6 +36,13 @@ no JS build step: plain forms with small progressive enhancements in `static/js/
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), `dev_server.py` (local run without Docker).