Commit Graph

319 Commits

Author SHA1 Message Date
hsiegeln
4d90d51501 feat(search): persistenter Thumbnail-Cache in SQLite, Default-TTL 30 Tage
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 54s
Vorher: In-Memory-Map, TTL 30 Minuten. Container-Neustart verwarf den
kompletten Cache, also musste nach jedem Deploy jede Suche wieder alle
Seiten laden.

Jetzt:
- Neue Tabelle thumbnail_cache (url PK, image, expires_at)
- Default-TTL 30 Tage, per Env KOCHWAS_THUMB_TTL_DAYS konfigurierbar
  (7, 365, was der User will — is alles ok laut Nutzer)
- Negative Cache: Seiten ohne Bild werden mit image=NULL gespeichert,
  damit wir nicht jede Suche die gleiche kaputte Seite wieder laden
- Lazy-Cleanup: pro searchWeb-Aufruf werden abgelaufene Zeilen via
  DELETE ... WHERE expires_at <= now() weggeräumt (Index-Scan, billig)

Migration 003_thumbnail_cache.sql: nicht-destruktiv, nur neue Tabelle.
Bestehende DB bekommt sie beim nächsten Start automatisch dazu.

Tests (99/99):
- Neuer Cache-Test: zweiter searchWeb für dieselbe URL macht keinen
  Page-Fetch mehr und liest die image-Spalte aus SQLite.
2026-04-17 18:34:29 +02:00
hsiegeln
1712263fd1 feat(search): HQ-Thumbnails durch immer aktive og:image-Extraktion
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 54s
Vorher: nur Treffer ohne SearXNG-Thumbnail wurden mit dem Seiten-Bild
angereichert. Treffer mit Thumbnail behielten das kleine 150-200 px-
Bildchen aus dem Such-Engine-Index.

Jetzt: Alle Treffer durchlaufen die Enrichment-Pipeline. Wenn die Seite
ein og:image/JSON-LD/Content-Bild hat (und das hat sie bei Rezept-Seiten
praktisch immer), wird das kleine SearXNG-Thumbnail damit überschrieben.
Wenn die Seite kein Bild liefert, bleibt das SearXNG-Thumbnail als
Fallback erhalten.

Das ist das gleiche Bild, das auch die Vorschau anzeigt — Suchergebnis
und Vorschau sind jetzt visuell konsistent.

Performance: Pro erster Suche bis zu ~6 Sekunden zusätzliche Latenz
(max 6 parallel, je 4 s Timeout). Der 30-min In-Memory-Cache macht
Wiederholsuchen instant.

Tests (98/98):
- Neu: SearXNG-Thumbnail wird durch og:image ersetzt.
- Neu: SearXNG-Thumbnail bleibt erhalten, wenn Seite kein Bild hat.
- Alt ("leaves existing thumbnails untouched") entfernt — Verhalten
  hat sich bewusst umgekehrt.
2026-04-17 18:31:42 +02:00
hsiegeln
53e4815508 chore(quotes): Ex-Spruch durch Mikrowellen-Pointe ersetzt
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 50s
2026-04-17 18:07:32 +02:00
hsiegeln
211d58ebec feat(search): Enter bleibt auf Seite + robustere Thumbnail-Erkennung
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 55s
Startseite:
- Enter/Return löst die Suche jetzt sofort aus (cancelt den Debounce),
  navigiert aber NICHT mehr auf /search. Der Anwender bleibt auf der
  gleichen Seite mit Inline-Ergebnissen.

Thumbnail-Enrichment (searxng.ts):
- Regex-basierte og:image-Extraktion durch linkedom-parseHTML ersetzt.
- Neue Fallback-Kette (in dieser Reihenfolge):
    1. <meta property/name = og:image | og:image:url | og:image:secure_url
                           | twitter:image | twitter:image:src>
    2. <link rel="image_src" href="...">
    3. JSON-LD image (auch tief in @graph; "image" als String, Array,
       Objekt-mit-url)
    4. Erstes <img> in article/main/.entry-content/.post-content/figure
- Relative URLs werden gegen die Seiten-URL zu absoluten aufgelöst
  (z.B. /uploads/foo.jpg → http://host/uploads/foo.jpg).
- maxBytes von 256 KB auf 512 KB angehoben, damit JSON-LD-lastige
  Recipe-Seiten nicht mitten im Script abgeschnitten werden.

Tests (97/97):
- Neu: JSON-LD-Image-Fallback-Test.
- Neu: Content-<img>-Fallback-Test mit relativer URL, die zur
  absoluten aufgelöst wird.
2026-04-17 18:04:59 +02:00
hsiegeln
9bc4465061 feat(home): zufälliger Spruch zwischen Titel und Suche
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 50s
Eine der 49 Flachwitze wird beim Laden der Startseite zufällig gewählt
und in kursiv unter "Kochwas" angezeigt. Die Auswahl passiert auf dem
Client (onMount), damit SSR und Hydration nicht miteinander streiten —
beim ersten Frame ist ein nicht-umbrechender Leerraum drin, damit das
Layout nicht springt.
2026-04-17 17:58:27 +02:00
hsiegeln
6a784488f5 fix(search): enrich missing SearXNG thumbnails with og:image
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 55s
SearXNG liefert je nach Seite mal ein thumbnail/img_src mit, mal nicht —
bei Chefkoch-Treffern hatten deshalb zufällig die Hälfte der Kacheln
einen Platzhalter, obwohl die Vorschau dann sehr wohl ein Bild fand.

searchWeb() holt jetzt für jeden Treffer ohne Thumbnail parallel
(max. 6 gleichzeitig, 4 s Timeout pro Request) die Seite und extrahiert
das og:image- oder twitter:image-Meta-Tag. Ergebnis wird 30 min
in-memory gecacht, damit wiederholte Suchen nicht wieder die gleichen
Seiten laden.

Tests:
- Neuer Test: Treffer ohne Thumbnail wird via og:image angereichert.
- Neuer Test: Treffer mit Thumbnail bleibt unverändert (keine Fetch).
- Bestehende Tests deaktivieren Enrichment via enrichThumbnails:false,
  damit sie keine echten Chefkoch-URLs aufrufen.
2026-04-17 17:55:53 +02:00
hsiegeln
3cd22544d3 feat(search): mobile header search expands on focus; drop hero button
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 52s
Home:
- "Suchen"-Button entfernt. Die Suche feuert ohnehin debounced beim
  Tippen; der Button war ein Relikt aus dem Submit-Modell. Enter auf
  dem Input löst weiterhin einen Submit aus (geht zur /search-Seite).

Header (< 520 px):
- Sobald das Suchfeld fokussiert wird, wandert das nav-search-wrap
  via :focus-within auf position: absolute und dehnt sich bis zum
  rechten Rand (1 rem Abstand) aus. Die Action-Icons werden dabei
  vom Suchfeld überlagert (z-index 60), sodass der Anwender auf
  engen Displays deutlich mehr Platz zum Tippen hat.
- Bar-Inner bekam position: relative, damit das absolute Ausdehnen
  innerhalb der Header-Zeile greift.
2026-04-17 17:52:51 +02:00
hsiegeln
d693cb422d feat(search): auto web search when no local hits, offer link otherwise
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 55s
Homepage (/):
- Keine lokalen Treffer → automatisch die Internet-Suche auslösen und
  die Ergebnisse als Karten unterhalb der Suche anzeigen.
- Mindestens ein lokaler Treffer → Karten zeigen + darunter ein
  dezenter Link "🌐 Im Internet weitersuchen" (geht zur /search/web
  Vollseite), keine automatische Internet-Suche.

Header-Dropdown (auf Rezept- und Vorschau-Seiten):
- Gleiche Logik: lokale Treffer oben + Fuß-Link; keine lokalen
  Treffer → Internet-Ergebnisse werden direkt im Dropdown angezeigt.
- Abschnittsüberschrift "Keine lokalen Rezepte – aus dem Internet:"
  trennt den Fallback visuell ab.

Race-Safety bleibt bestehen: Query-Vergleich vor jedem State-Write,
sodass spät ankommende Antworten keinen neueren Suchstand überschreiben.
2026-04-17 17:47:26 +02:00
hsiegeln
76110f9841 fix(nav): right-align action icons even when search is hidden
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
Auf Nicht-Rezept-Seiten (Home, Wishlist, Admin) ist das Header-Suchfeld
ausgeblendet. Ohne flex-Spacer rutschten 🍽️/⚙️/Profil direkt neben das
Brand-Badge — besonders auffällig im Mobile-Layout.

margin-left: auto auf .bar-right schiebt die Action-Icons immer an den
rechten Rand, unabhängig davon ob das Suchfeld sichtbar ist.
2026-04-17 17:43:08 +02:00
hsiegeln
d737618312 feat(search): live debounced search with inline hits and header dropdown
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
Homepage (/):
- Tippen > 3 Zeichen + 300 ms Debounce → lokale Suche feuert automatisch
- Treffer erscheinen direkt unter dem Suchfeld als Karten-Grid
- "Zuletzt hinzugefügt" wird ausgeblendet, sobald aktiv gesucht wird
- 0 Treffer + fertig gesucht → Inline-Button "Im Internet weitersuchen"

Header (nur auf /recipes/[id] und /preview):
- Gleiche Debounce-Logik, aber Treffer in einem Dropdown unterm Feld
- Dropdown: kompakte Zeilen mit Thumbnail, Titel, Domain
- Fußzeile des Dropdown: "Im Internet weitersuchen"
- Click-outside und Escape schließen das Dropdown
- afterNavigate setzt Query nach dem Klick auf einen Treffer zurück
- Header-Breite ist jetzt auf 760 px begrenzt (gleich wie Rezept-Content),
  damit die Suchleiste nie breiter wird als das Rezept darunter

Race-Safety: Ein zweites Tippen während laufender Fetch überschreibt
die Ergebnisse des ersten Requests nicht (Query-Vergleich vor Write).
2026-04-17 17:41:10 +02:00
hsiegeln
84655151be feat(nav): search lives in the header on all non-home pages
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
- "← Lokale Suche"-Breadcrumb auf der Web-Suchseite entfernt (überflüssig,
  da die lokale Suche automatisch zur Web-Suche weiterleitet, wenn leer).
- Header-Bar enthält jetzt ein Pill-Suchfeld, das von jeder Unterseite
  aus direkt auf /search?q=... navigiert — kein Zurück mehr nötig,
  wenn man aus einem offenen Rezept weiter sucht.
- Auf der Startseite bleibt die große Hero-Suche; das Header-Feld ist
  dort ausgeblendet, damit es keine doppelte Eingabestelle gibt.
- Auf /search und /search/web spiegelt das Header-Feld die aktuelle
  Query wider, sodass man den Begriff verfeinern kann.
- Mobile < 520px: Brand schrumpft zu einem 🍳-Badge, damit Platz für
  das Suchfeld + Icons bleibt.
2026-04-17 17:31:08 +02:00
4f7c76c908 feat(ui): custom dialog replaces all remaining window.alert() calls
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
alertAction({title, message}) returns Promise<void> and renders the
same ConfirmDialog with infoOnly:true — single OK button, no Abbrechen.
Replaces:
- 'Bitte Profil wählen.' (recipe rating / favorite / cooked / comment)
- 'Bitte Profil wählen, um zu liken.' (wishlist)
- 'Profil konnte nicht angelegt werden' (ProfileSwitcher)
- 'Umbenennen fehlgeschlagen' (admin/profiles)
- 'Speichern fehlgeschlagen' (preview)

No window.alert() or window.confirm() left in the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:23:07 +02:00
1b9928f806 feat(ui): custom confirmation dialog replacing native window.confirm
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 51s
Single reusable dialog with a promise-based API: confirmAction({...})
returns Promise<boolean>. Supports title, optional message body,
confirm/cancel labels, and a 'destructive' flag that paints the confirm
button red.

Accessibility: Escape cancels, Enter confirms, confirm button auto-focus,
role=dialog + aria-labelledby, backdrop click = cancel.

Rolled out to: recipe delete, domain remove, profile delete, wishlist
remove. Native confirm() is gone from the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:15:21 +02:00
3b1950713f feat(ui): wishlist page, recipe toggle button, header link
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 54s
- /wishlist renders cards with avatar-badge of who added it, like count,
  heart toggle for active profile, delete button. Sort dropdown switches
  between popular / newest / oldest.
- /recipes/[id] gets 'Auf Wunschliste (setzen)' button alongside favorite.
- Layout header shows 🍽️ link to /wishlist next to the admin ⚙️.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:08:22 +02:00
28e40d763d feat(api): wishlist endpoints (list, add, remove, like, unlike)
GET /api/wishlist?sort=popular|newest|oldest&profile_id=…
POST /api/wishlist { recipe_id, profile_id? }
DELETE /api/wishlist/[recipe_id]
PUT    /api/wishlist/[recipe_id]/like { profile_id }
DELETE /api/wishlist/[recipe_id]/like { profile_id }

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:08:22 +02:00
18547a7301 feat(wishlist): add shared family wishlist with likes
Each recipe appears at most once on the wishlist. Any profile can add,
remove, like, and unlike. Ratings and cooking log stay independent.

Data model: wishlist(recipe_id PK, added_by_profile_id, added_at)
            wishlist_like(recipe_id, profile_id, created_at)

Why: 'das will ich essen' — family members pick candidates, everyone
can +1 to signal agreement, cook decides based on popularity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:08:22 +02:00
72019f9cb7 fix(ui): show remote image URL in preview, local file after save
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 51s
In the preview flow, recipe.image_path is still the external recipe-page
URL (we only download the image when persisting). The RecipeView
component always prefixed with /images/ and produced a 404.

Now RecipeView detects http(s):// prefix and uses it directly, else
treats the value as a local filename under /images/.

Also adds referrerpolicy=no-referrer on the preview image so remote
CDNs don't block us based on Origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:03:13 +02:00
570a524d86 fix(search): unblock SearXNG 403 — config + headers
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
SearXNG returned 403 for every query, logging
'X-Forwarded-For nor X-Real-IP header is set!'. Two fixes, both needed:

1. searxng/settings.yml was being overwritten by SearXNG's default
   config in fresh volumes. Explicitly set limiter: false,
   public_instance: false, and move secret_key to env lookup via
   ${SEARXNG_SECRET:-…}. Force a well-known JSON format list.

2. Even with the limiter off, SearXNG's bot detection still nags on
   missing forwarder headers. The Node client now sends
   X-Forwarded-For: 127.0.0.1, X-Real-IP: 127.0.0.1 and Accept: json
   deterministically. Done via a new extraHeaders option on the http
   wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:56:13 +02:00
24058bcb77 fix(docker): healthcheck uses 127.0.0.1, not localhost
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 54s
In the alpine runtime 'localhost' resolves to ::1 (IPv6) first. The
SvelteKit Node adapter binds to 0.0.0.0 which is IPv4-only, so wget to
'localhost:3000' fails with 'Connection refused'. Traefik then filters
the container as unhealthy and no router is registered.

Using 127.0.0.1 makes the probe deterministically hit the bound IPv4
socket.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:47:48 +02:00
ec902049c1 fix(docker): tighter healthcheck cadence with start-period
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
Traefik filters containers that are 'unhealthy' or still 'starting'
(no health result yet). The old 30s interval meant kochwas stayed in
'starting' for 30+ seconds after boot, blocking Traefik from routing to it.

New timing:
  --start-period=20s      grace window — failures don't mark unhealthy yet
  --start-interval=2s     fast probes during start-period
  --interval=15s          steady-state cadence after first success
  --timeout=5s, retries=3 unchanged

Container becomes 'healthy' within ~2-5s of the app coming up, so Traefik
picks it up almost immediately after 'docker compose up'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:45:00 +02:00
88125935d1 ci: switch build cache from type=gha to type=registry
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
The Gitea Actions cache HTTP backend times out on export
(dial tcp 172.20.8.x:xxxxx: i/o timeout), failing the job even though
the image push itself succeeds.

Registry cache stores layer cache as a dedicated tag ':buildcache' in
the same container registry that already holds the images — reliable
and self-contained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:16:07 +02:00
f7fe216089 ci: use REGISTRY_TOKEN PAT for Gitea container registry login
Some checks failed
Build & Publish Docker Image / build-and-push (push) Failing after 24s
The auto-issued GITEA_TOKEN in Actions does not carry write:package scope,
so the docker login step failed with 'unauthorized'. Switching to a user-
supplied secret REGISTRY_TOKEN (PAT with write:package + read:package).

Setup on Gitea side:
1. Profile → Settings → Applications → Generate New Token
   with scopes write:package + read:package.
2. Repo → Settings → Actions → Secrets → add REGISTRY_TOKEN = <that PAT>.
   Optional: REGISTRY_USER if the owning account differs from gitea.actor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:10:00 +02:00
bf12468103 fix(infra): rename external network to traefik_proxy matching homelab setup
Some checks failed
Build & Publish Docker Image / build-and-push (push) Failing after 25s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:06:38 +02:00
32e7e54f4e feat(infra): add Traefik labels and network topology for homelab ingress
Some checks failed
Build & Publish Docker Image / build-and-push (push) Failing after 27s
- kochwas service gets Traefik v2 labels matching the project's conventions:
  websecure entrypoint, cloudflareResolver, Host(`kochwas.siegeln.net`).
- Service port 3000 exposed to Traefik only; the external port binding is gone.
- Dual network: external 'proxy' (for Traefik ingress) and internal 'internal'
  (for kochwas ↔ searxng). traefik.docker.network hint is set.
- SearXNG has no Traefik labels — intentionally only reachable from kochwas.

Note: the 'proxy' network name must match the existing external Traefik network
(change via 'name:' field if your homelab uses a different one like 'traefik').

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:05:06 +02:00
5fbb90745d ci: build for linux/arm64 (Raspberry Pi 5 target)
Some checks failed
Build & Publish Docker Image / build-and-push (push) Failing after 3m48s
Runner is arm64, so native build is much faster than amd64-via-QEMU.
Dev/test amd64 images can still be built locally with 'docker build'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:57:55 +02:00
3eb89cc5d6 ci: add Gitea Actions workflow to build and push Docker image
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
On push to main (or version tag), the workflow logs into the Gitea container
registry, builds a multi-tag image (sha-<short>, branch name, 'latest' on main,
version on tag) and pushes to gitea.siegeln.net/<owner>/<repo>.

docker-compose.prod.yml now pulls from the registry by default, with
KOCHWAS_TAG env var to pin a specific build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:54:34 +02:00
8df18218ca feat(search): auto-redirect to web search when no local hits
Spares the user a click: empty local result set triggers goto('/search/web?q=...')
with replaceState so the back button returns to the previous page, not the empty
local results list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:49:24 +02:00
7c0fb27b52 fix(ui): make preview error more actionable
Explains that the page was likely a forum/listing, not a recipe, and
offers 'Zurück zur Trefferliste' plus 'Seite im Browser öffnen' as escape hatches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:47:28 +02:00
041ef12582 fix(search): filter forum/magazin/listing URLs from web search results
Blocks common non-recipe paths like /forum/, /magazin/, /suche/, /themen/,
Chefkoch's /rs/s\d+/ search URLs and /Rezepte.html listings.

Before: 'ravioli' search returned forum threads and listing pages that
triggered 'No schema.org/Recipe JSON-LD' on preview.
After: only real recipe URLs pass through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:47:28 +02:00
bce9e87095 docs: update session handoff — MVP complete across all 6 phases
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:42:29 +02:00
ddb3419cd9 feat(infra): add production Dockerfile and docker-compose.prod.yml
Multi-stage alpine-based build, healthcheck on /api/health, /data volume.
Verified end-to-end: image builds, container starts, health returns 200.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:41:20 +02:00
a22fb86479 feat(print): add print-optimized route with server-side portion scaling
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:41:20 +02:00
0635c2702a feat(pwa): add web manifest, SVG icon, and offline service worker
Service worker caches app shell + images for offline recipe access in the kitchen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:41:20 +02:00
ea9a79226a feat(db): bundle migration SQL via Vite glob + auto-create data dirs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:41:20 +02:00
0f9aabe76b refactor: move scaler out of $lib/server so it can run in browser
RecipeView needs scaleIngredients on the client for live portion scaling.
Moved scaler.ts from $lib/server/recipes/ to $lib/recipes/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:41:20 +02:00
4d7783dd8b feat(ui): add admin area (domains, profiles, backup) with gear link in header
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:35:20 +02:00
2ca1bbaf07 feat(backup): add ZIP export endpoint (DB + images)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:35:19 +02:00
3207166fe8 chore(deps): add archiver + yauzl for backup ZIPs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:35:19 +02:00
41dbb81a54 feat(ui): add web search page and preview-before-save flow
- /search shows 'Im Internet suchen' CTA when no local hits or always after search
- /search/web lists SearXNG hits with domain pill and snippet
- /preview loads recipe via preview endpoint and shows unified RecipeView with banner
- Save button imports via POST and navigates to the saved recipe

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:33:21 +02:00
52c25fdd2c feat(search): add SearXNG client with whitelist-filtered web search
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:33:21 +02:00
4d5e0aa963 feat(ui): add layout with profile bar, home, search, recipe pages
- Homepage with search and recent recipes
- Search page listing local hits (FTS5)
- Recipe page with ratings, favorites, cooking log, comments
- Wake-Lock on recipe view for mobile kitchen use

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:28:22 +02:00
8df09b1134 feat(ui): add unified RecipeView component with tabs and portion scaling
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:28:22 +02:00
974e3cf7ef feat(ui): add ProfileSwitcher modal and StarRating component
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:28:21 +02:00
e90545a637 feat(ui): add profile client store with LocalStorage persistence
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:28:21 +02:00
1adc0ee021 feat(api): serve local images with cache headers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:28:21 +02:00
45275e56a9 feat(api): add recipe detail, search, rating, favorite, cooked, comments endpoints
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:23:00 +02:00
7c62c977c4 feat(recipes): add local search (FTS5 bm25) and action handlers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:23:00 +02:00
75c96d12e9 docs: session handoff after Phase 1+2 autonomous work
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:13:43 +02:00
86ff4c141a feat(api): expose preview/import/profile/domain endpoints
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:12:59 +02:00
5693371673 feat(profiles): add profile repository
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:11:23 +02:00