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).
- "← 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.
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>
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>
- /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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- /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>
- 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>