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
+41
View File
@@ -0,0 +1,41 @@
"""Generates the PWA icons in static/icons (run once; output is committed)."""
from pathlib import Path
from PIL import Image, ImageDraw
OUT = Path(__file__).resolve().parent.parent / "static" / "icons"
ACCENT = (180, 83, 42)
CREAM = (250, 247, 242)
def draw_pot(size, padding):
img = Image.new("RGB", (size, size), ACCENT)
d = ImageDraw.Draw(img)
s = size - 2 * padding
x0, y0 = padding, padding
# pot body
d.rounded_rectangle([x0 + s * 0.18, y0 + s * 0.42, x0 + s * 0.82, y0 + s * 0.82], radius=s * 0.08, fill=CREAM)
# rim
d.rounded_rectangle([x0 + s * 0.12, y0 + s * 0.36, x0 + s * 0.88, y0 + s * 0.45], radius=s * 0.04, fill=CREAM)
# handles
d.rounded_rectangle([x0 + s * 0.05, y0 + s * 0.5, x0 + s * 0.2, y0 + s * 0.58], radius=s * 0.03, fill=CREAM)
d.rounded_rectangle([x0 + s * 0.8, y0 + s * 0.5, x0 + s * 0.95, y0 + s * 0.58], radius=s * 0.03, fill=CREAM)
# steam
for i, dx in enumerate((0.35, 0.5, 0.65)):
top = y0 + s * (0.12 if i == 1 else 0.17)
d.line([x0 + s * dx, top, x0 + s * dx, y0 + s * 0.3], fill=CREAM, width=max(2, int(s * 0.045)))
return img
def main():
OUT.mkdir(parents=True, exist_ok=True)
draw_pot(192, 16).save(OUT / "icon-192.png")
draw_pot(512, 40).save(OUT / "icon-512.png")
draw_pot(512, 110).save(OUT / "icon-maskable-512.png") # content inside the safe zone
draw_pot(180, 14).save(OUT / "apple-touch-icon.png")
draw_pot(64, 4).save(OUT / "favicon.png")
if __name__ == "__main__":
main()