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:
co-authored by
Claude Opus 5.5
parent
1bf8ef8b65
commit
60785e2437
+48
@@ -0,0 +1,48 @@
|
||||
"""Web app manifest and service worker (installable app, Android share target)."""
|
||||
|
||||
from django.contrib.auth.decorators import login_not_required
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.http import require_GET
|
||||
|
||||
# No caching of pages or API responses: private recipes must never sit in a shared cache,
|
||||
# and the app is online-only. The worker exists so browsers offer installation.
|
||||
SERVICE_WORKER = """self.addEventListener("install", () => self.skipWaiting());
|
||||
self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
|
||||
"""
|
||||
|
||||
|
||||
@login_not_required
|
||||
@require_GET
|
||||
@cache_control(max_age=3600)
|
||||
def manifest(request):
|
||||
data = {
|
||||
"name": "Kochbuch",
|
||||
"short_name": "Kochbuch",
|
||||
"lang": "de",
|
||||
"start_url": reverse("recipes:list"),
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#faf7f2",
|
||||
"theme_color": "#b4532a",
|
||||
"icons": [
|
||||
{"src": static("icons/icon-192.png"), "sizes": "192x192", "type": "image/png"},
|
||||
{"src": static("icons/icon-512.png"), "sizes": "512x512", "type": "image/png"},
|
||||
{"src": static("icons/icon-maskable-512.png"), "sizes": "512x512", "type": "image/png", "purpose": "maskable"},
|
||||
],
|
||||
"share_target": {
|
||||
"action": reverse("recipes:share"),
|
||||
"method": "GET",
|
||||
"params": {"title": "title", "text": "text", "url": "url"},
|
||||
},
|
||||
}
|
||||
return JsonResponse(data, content_type="application/manifest+json", json_dumps_params={"ensure_ascii": False})
|
||||
|
||||
|
||||
@login_not_required
|
||||
@require_GET
|
||||
@cache_control(max_age=0, no_cache=True)
|
||||
def service_worker(request):
|
||||
return HttpResponse(SERVICE_WORKER, content_type="application/javascript")
|
||||
+3
-1
@@ -1,9 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from . import pwa, views
|
||||
|
||||
app_name = "core"
|
||||
|
||||
urlpatterns = [
|
||||
path("healthz", views.healthz, name="healthz"),
|
||||
path("manifest.webmanifest", pwa.manifest, name="manifest"),
|
||||
path("sw.js", pwa.service_worker, name="service_worker"),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user