- src/lib/server/api-helpers.ts mit parsePositiveIntParam(),
validateBody<T>() und ErrorResponse type
- 13 unit tests fuer die beiden helper (HttpError-Shape verifiziert)
- Konsolidiert spaeter 9x parseId und 11x safeParse-Bloecke aus den
+server.ts handlern
Findings aus REVIEW-2026-04-18.md (Refactor A) und redundancy.md
- src/lib/constants.ts: SW_VERSION_QUERY_TIMEOUT_MS, SW_UPDATE_POLL_INTERVAL_MS
- pwa.svelte.ts: nutzt die Konstanten statt 1500/30*60_000
- cache-strategy.ts / diff-manifest.ts: RequestShape/ManifestDiff entkapselt
(intern statt export, da nirgends extern importiert)
- recipes/[id]/image: deutsche Fehlermeldungen auf Englisch (Konsistenz
mit allen anderen Endpoints)
Findings aus REVIEW-2026-04-18.md (Quick-Wins 6+7) und dead-code.md
- Neuer Endpoint POST/DELETE /api/recipes/:id/image.
* Multipart-Upload mit Feld "file".
* Whitelist: JPG, PNG, WebP, GIF, AVIF. Max 10 MB.
* Dedupe per SHA-256-Filename analog zu downloadImage().
- updateImagePath()-Repo-Funktion ergänzt.
- RecipeEditor: neuer Block "Bild" ganz oben. Preview + Buttons
"Hochladen"/"Ersetzen"/"Entfernen". Upload passiert direkt beim
Auswählen, nicht erst bei "Speichern" — das Bild ist eigene
Ressource, Abbrechen rollt es nicht zurück (okay, da dedupliziert).
- onimagechange-Callback informiert die Detail-Ansicht, damit die
Preview im RecipeView auch nach Abbrechen aktuell bleibt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Dekorativer GripVertical raus, stattdessen zwei Pfeil-Buttons (↑/↓)
pro Zeile. An erster/letzter Stelle sind die Buttons disabled.
- moveIngredient() vertauscht Zeile mit Nachbarn; simpel und
tastatur-/touch-freundlich ohne Drag-and-Drop-Abhängigkeit.
- Grid-Spalten von 1fr 90px (Zutat/Notiz) auf 1fr 1fr — beide Felder
sind jetzt gleich breit, wie im Family-Feedback gewünscht.
- Mobile-Layout behält gestaffelte Note-Zeile, Move-Spalte rutscht
als eigene Spalte links daneben.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auf dem 10"-Tablet war die Schrift in Zutaten und Zubereitung zu klein.
Im 2-Spalten-Layout (>=820px) bumpen wir jetzt:
- Zutaten-Zeilen und Step-Text auf 1.2rem (vorher 1rem)
- qty-Spalte breiter (6rem statt 5rem)
- Portionen-Zahl größer
- Step-Badge auf 2.4rem + 1.1rem Font
Mobile bleibt unverändert — Lesedistanz ist dort anders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nach dem DDG-Rauswurf war Mojeek die verbleibende Lärm-Quelle im Log:
HTTP 403 pro Suche, suspended_time=180. Mojeek hat nach eigenem Muster
Pi-IPs als automatisierten Traffic klassifiziert. Brave (API) deckt die
Websuche zuverlässig ab — Mojeek ist draußen, sowohl im searxng.ts-
Query (engines=brave) als auch in der SearXNG-keep_only-Liste.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zwei Fixes gegen die hartnäckigen DDG-CAPTCHA-Fehler im SearXNG-Log:
1. searxng.ts fragt jetzt explizit `engines=brave,mojeek` an.
Vorher wurde nur `categories=general` gesetzt — dadurch wurden
alle in dieser Kategorie aktivierten Engines abgefragt, inkl. DDG
(das trotz `disabled: true` weiter antwortete).
2. settings.yml nutzt `use_default_settings.engines.keep_only` statt
einzelner `disabled: true`-Overrides. SearXNGs Merge-Semantik für
partielle Engine-Overrides (nur name + disabled ohne engine:)
greift in der aktuellen Version nicht zuverlässig, deshalb kam
DDG durch. keep_only wirft alles außer brave+mojeek vor dem Laden
raus — kein Captcha-/403-Log-Lärm mehr.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Die SearXNG-Engine "brave" ist ein HTML-Scraper von search.brave.com
und ignoriert den api_key-Parameter. Dadurch liefen alle Anfragen
gegen den gescrapten Web-Endpoint, der aus dem Pi-Netz regelmäßig
rate-limited wurde (SearxEngineTooManyRequestsException, 60%).
Fix: engine: braveapi nutzen. Das ist die offizielle Brave-Search-API-
Engine, die den api_key als X-Subscription-Token-Header sendet.
Der Key steht unverändert in .env auf dem Pi und wird vom
searxng-init-Container ins gerenderte settings.yml expandiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Commit 1bec054 hatte einen globalen controllerchange-Listener in
init(), der bei jedem Event location.reload() auslöste. In Kombination
mit der Zombie-Aufräumung (silent SKIP_WAITING) ergab das einen
Endlos-Loop: Seite lädt → Zombie erkannt → SKIP_WAITING → controller-
change → Reload → neue Seite mit frischem Zombie → usw.
Fix: Der controllerchange-Listener wird nur noch scoped aus reload()
heraus gesetzt ({ once: true }) — also genau dann, wenn der User auf
„Neu laden" geklickt hat und einen Reload tatsächlich will. Beim
silent Zombie-Cleanup gibt es keinen Listener, die Seite läuft
einfach nahtlos unter dem neuen (funktional identischen) SW weiter.
Regression-Test sichert ab, dass fireControllerChange() nach silent
SKIP_WAITING location.reload() NICHT aufruft.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Das reine Workbox-Handshake-Pattern aus c2074c9 reicht für dieses
Deploy nicht. Live-Analyse mit Playwright ergibt reproduzierbar nach
dem Reload-Klick:
- active-SW: Version 1776527907402
- waiting-SW: Version 1776527907402 (bit-identisch!)
- Nur ein einziger shell-Cache
- Server-Response: gleiche Version
→ Toast kommt bei jedem Reload erneut.
Vermutung: Race zwischen Chromium-SW-Update-Check (der parallel
zum SKIP_WAITING läuft) und activate. Der Browser hält den zweiten
Installation-Versuch mit identischen Bytes im waiting-Slot.
Fix: SW bekommt GET_VERSION-Handler, Client fragt via MessageChannel
active und waiting nach Version. Bei Gleichheit räumt er den Zombie
stumm auf (SKIP_WAITING ohne Toast), bei Versions-Unterschied
zeigt er den Toast. Der refreshing-Flag-Reload-Guard aus c2074c9
bleibt erhalten.
Industry-Standard-Pattern bleibt die Basis; GET_VERSION ist ein
defensiver Zusatz für einen reproduzierbaren Browser-Edge-Case,
den Workbox nicht abfängt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Zombie-Version-Check (858d4c1) ging über das Standard-Handshake-
Pattern hinaus. User will Industry-Standard: Workbox/web.dev-Pattern
ohne GET_VERSION-Sonderlocke.
Änderungen:
- service-worker.ts: GET_VERSION-Handler entfernt. SW reagiert nur
noch auf SKIP_WAITING.
- pwa.svelte.ts: queryVersion + evaluateWaiting entfernt. init()
zeigt Toast wieder schlicht bei registration.waiting (das ist
kanonisch — bit-gleiche Bytes erzeugen keinen waiting-Slot).
- controllerchange-Listener wandert nach init() mit refreshing-Flag
(CRA-Idiom): verhindert Doppel-Reload, wenn User zusätzlich F5
drückt, und stellt sicher, dass der Listener in _jeder_ Session
aktiv ist, nicht erst nach dem ersten reload()-Call.
- pwa-store.test.ts: Tests decken jetzt waiting→Toast, no-waiting→
kein Toast, Handshake, refreshing-Flag und Sofort-Reload ab.
Der Zombie-Edge-Case (Browser-Quirk mit bit-identischem waiting-SW)
wird sich nach einmaligem Klick auflösen — erwarteter Trade-off
gegenüber der eingesparten Komplexität.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der vorige Fix (3d6f639) hat den Endlos-Toast "Neue Kochwas-Version
verfügbar" im Happy-Path beseitigt, aber das Grundproblem blieb:
pwaStore.init() hat blind `registration.waiting` als Update-Signal
verwendet.
Beobachtet auf der Live-PWA: Nach dem Reload-Klick existiert
registration.waiting weiter — als bit-identischer Zombie zum aktiven
SW (nur ein einziger shell-Cache `kochwas-shell-<version>`, Server-
Fetch liefert dieselbe Version-Konstante wie der active-SW). Der
Browser räumt diesen waiting-Slot nicht von selbst auf. Ergebnis:
beim nächsten init() steht `registration.waiting` wieder, Toast
kommt wieder.
Fix: SW bekommt einen GET_VERSION-MessageHandler. pwaStore fragt
active und waiting per MessageChannel nach ihrer Version. Sind sie
gleich, schickt er SKIP_WAITING silent an den Zombie und zeigt
KEINEN Toast. Nur bei echter Versions-Differenz erscheint das Update-
Angebot. Der alte onUpdateFound-Pfad geht den gleichen Weg.
Regression-Test: tests/unit/pwa-store.test.ts deckt Zombie-, Echt-
Update- und Fallback-Fall (alter SW ohne GET_VERSION) ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Das Schema hatte positive() statt nonnegative() — damit schlug
jedes Save fehl, bei dem der Importer keine Portionsangabe finden
konnte und 0 eingesetzt hatte (z.B. bei rezeptwelt.de-Rezepten).
Alle anderen Int-Felder im gleichen Schema nutzen nonnegative()
konsistent; servings_default war der Ausreißer. DB-Spalte erlaubt
0 ohnehin, insertRecipe akzeptiert 0 → nur die PATCH-Validierung
hat unnötig blockiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der SW rief bisher im Install-Handler self.skipWaiting() auf —
der neue SW übersprang damit die "waiting"-Phase und aktivierte
sofort. pwaStore.onUpdateFound feuerte trotzdem auf statechange=
"installed" + vorhandenem controller und setzte updateAvailable=
true. Ergebnis: Toast erschien, obwohl der SW bereits übernommen
hatte, und der Klick auf "Neu laden" löste durch das Timing einen
neuen Update-Zyklus aus → Endlosschleife, v.a. im Incognito-Mode
wo jede Session neu installiert.
Jetzt klassisches Pattern: SW wartet in "installed"-Zustand bis
der User den Toast bestätigt; pwaStore.reload() postet
SKIP_WAITING an den wartenden SW, lauscht auf controllerchange
und reloadet dann erst. Ohne diese Trennung ist der Toast
semantisch kaputt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CLAUDE.md: zwei neue Gotchas (SW nur HTTPS, Icon-Rendering) +
Erweiterung der "Dateien, die man anfasst"-Liste um SW-Pfade und
Client-Stores.
OPERATIONS.md: neuer Abschnitt "PWA / Offline-Modus" mit Caches,
Sync-Verhalten, Debug-Pfad und E2E-Test-Kommando.
ARCHITECTURE.md: neuer Abschnitt "Service Worker (PWA)" mit
Zuständigkeiten, Cache-Strategien, Message-Protokoll, Stores und
Komponenten.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Playwright-Spec mit drei Tests:
- Offline-Navigation zu einem Rezept-Detail (Cache-Lesefall)
- Schreib-Aktion offline zeigt Toast (Favorit-Klick → Fehler-Toast)
- SyncIndicator zeigt "Offline"-Pill bei deaktiviertem Netzwerk
Seed-Fixture legt per /api/recipes/blank ein Rezept an, falls die
DB leer ist. waitForSync pollt navigator.serviceWorker.ready und
wartet zusätzlich 3 s für den initialen Pre-Cache.
Profil-Fixture (worker-scoped) erstellt bei Bedarf ein Test-Profil
und setzt es per addInitScript in localStorage, damit der Favorit-
Button den requireOnline-Guard erreicht (statt alertAction-Dialog).
SyncIndicator-Test ohne Reload: network.svelte.ts lauscht direkt auf
den 'offline'-Browser-Event, der bei context.setOffline(true) feuert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@playwright/test + chromium als devDep. playwright.config.ts
startet npm run preview (production build nötig für SW), baseURL
localhost:4173, fullyParallel off (SW-Installations-Timing). Ein
Smoke-Test smoke.spec.ts öffnet / und prüft den Titel. test-results/
und playwright-report/ in .gitignore ergänzt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer vierter Admin-Tab (Smartphone-Icon) mit drei Karten:
1. Installieren — fängt beforeinstallprompt (Android), zeigt
iOS-Teilen-Hinweis, sonst Info "nicht verfügbar".
2. Offline-Synchronisation — Status + "Jetzt synchronisieren"-
Button, disabled wenn offline.
3. Cache — "Offline-Cache leeren" löscht alle kochwas-*-Caches
via caches.keys() + delete.
install-prompt.svelte.ts hält das deferred-Event und die Plattform
(android/ios/other) per UA-Detection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer Helper requireOnline(action) prüft vor jedem Schreib-Fetch
den Online-Status. Offline: ein Toast erscheint ("Die Aktion braucht
eine Internet-Verbindung."), Aktion bricht sauber ab. Der Button-
State bleibt unverändert (kein optimistisches Update, das gleich
wieder zurückgedreht werden müsste).
Eingebaut in Rezept-Detail (8 Handler), Register (2), Wunschliste
(2), Admin Domains/Profile/Backup, Home-Dismiss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorher: saveCachedIds(currentIds) hat alle Server-IDs gespeichert,
auch wenn cacheRecipe still fehlschlug — beim nächsten sync-check
wurden die fehlenden übersprungen, offline gab's 404. Jetzt:
cacheRecipe + addToCache geben boolean zurück, nur die wirklich
erfolgreich gecachten IDs landen im Manifest. Bei Update-Sync wird
das Manifest aus (cached - toRemove + successful) berechnet.
Zusätzlich console.warn in addToCache, damit Cache-Misses auf dem
Pi debuggbar sind.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Message-Handler für sync-start (initial: alle Rezepte cachen) und
sync-check (delta: nur neue nachladen, gelöschte räumen). Vor dem
Sync ein Storage-Quota-Check (<100 MB frei → abbrechen mit Fehler-
Broadcast). Concurrency-Pool mit 4 parallelen Downloads pro
Rezept (HTML, API-JSON, Bild). Fortschritt per postMessage an
alle Clients, die über den sync-status-Store den SyncIndicator
füllen. Das Cache-Manifest wird als JSON-Response unter
/__cache-manifest__ im kochwas-meta Cache persistiert.
Client triggert beim App-Start entweder sync-check (bereits
kontrollierter SW) oder sync-start (erstmaliger SW-Install).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/service-worker.ts installiert die App-Shell-Assets (build +
files aus $service-worker) beim install-Event in kochwas-shell-
<version>, räumt alte Shell-Caches beim activate und dispatcht
jeden Fetch via resolveStrategy — shell/images cache-first, swr
stale-while-revalidate, network-only unangetastet. Pre-Cache-
Orchestrator kommt in Task 9.
Client-seitig registriert sw-register.ts den SW und verdrahtet
Messages vom SW in den sync-status-Store.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure Funktion diffManifest(current, cached) → {toAdd, toRemove}.
Vom SW beim Update-Sync genutzt: neue Rezept-IDs nachladen,
gelöschte aus dem Cache räumen. 5 Tests decken add/remove/
beides/unchanged/empty-cache ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bottom-right Pill zeigt Sync-Fortschritt (Sync N/M) oder Offline-
Status. Klick öffnet Overlay mit "Zuletzt synchronisiert: vor
N Min" + manuellem Refresh-Button (postMessage type=sync-check an
den SW). prefers-reduced-motion noch nicht gehandhabt — Spin-Icon
dreht sich bewusst; kein UX-Schaden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spiegelt SW-Messages (sync-start/progress/done/error) in einen
Svelte-State. lastSynced wird in localStorage persistiert, damit
der User nach einem Reload sieht, wann zuletzt synchronisiert
wurde. Wird vom SyncIndicator und der Admin-App-Tab konsumiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Toast-Queue als $state-Store mit Auto-Dismiss nach 3 s und manuellem
dismiss(id). Drei Kinds: info/error/success (Farbe). Renderer als
<Toast /> im Root-Layout, fix-positioniert oben mittig. Wird
vom Offline-Check der Schreib-Aktionen genutzt und später auch für
Sync-Abschluss-Meldungen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reaktiver Store basierend auf navigator.onLine und den window-
Events online/offline. Kein aktives Heuristik-Probing — für
unseren Offline-PWA-Use-Case reicht der Browser-Status. Wird von
SyncIndicator und require-online-Helper konsumiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Android Chrome bevorzugt für den Home-Screen rasterbare PNG-Icons
über reines SVG. 192×192 und 512×512 werden aus static/icon.svg
per Sharp-Skript gerendert (npm run render:icons) und committet,
damit CI keine zusätzliche Abhängigkeit hat. Manifest referenziert
alle drei Icons mit purpose "any maskable" → rund-/squircle-sicher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ergebnis des Brainstormings. Entscheidungen:
- Alle Rezepte + Bilder synchronisieren (~60 MB, ~200 Rezepte)
- SvelteKits eingebauter Service Worker, keine externe PWA-Abhängigkeit
- Hintergrund-Pre-Cache ohne Blocker, sichtbarer Fortschritt im
dezenten Sync-Indikator unten rechts
- Stale-While-Revalidate für Rezept-Daten, Cache-First für Shell+Bilder
- Schreib-Aktionen offline: proaktiver Check + Toast, keine Queue
- Neuer Admin-Tab "App" für Install-Button, Sync-Status, Reset
- Unit-Tests für Cache-Strategy/Diff, Playwright-E2E für Offline-Flows
Bereit für Nutzer-Review und anschließende Plan-Erstellung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der User pastet bewusst eine URL und erwartet, dass der Import
klappt — die Whitelist-Prüfung (DOMAIN_BLOCKED) im previewRecipe
war da nur Reibung. Die Whitelist bleibt für die Web-Suche relevant
(dort muss das Crawl-Feld eingeschränkt werden), für Imports nicht
mehr.
Dropped: isDomainAllowed + whitelist.ts, DOMAIN_BLOCKED-Code in
ImporterError, die zugehörige Branch in mapImporterError. Tests
entsprechend angepasst: statt "DOMAIN_BLOCKED wenn nicht whitelisted"
prüft der Preview-Test jetzt "klappt auch ohne Whitelist-Eintrag".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Beliebteste" ist mehrdeutig (Sterne? Favoriten? Wünsche?). Der
Sort-Key popular sortiert nach wanted_by_count DESC, also der
Anzahl Profile mit dem Rezept auf ihrer Wunschliste. Das Label
macht das jetzt eindeutig.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Das native <select> fiel stilistisch aus dem App-Bild. Jetzt
identisch zur "Alle Rezepte"-Sortierung auf der Startseite: drei
grüne Pill-Chips (Beliebteste / Neueste / Älteste), aktive Pille
invertiert. Verhalten gleich, nur die Optik angepasst.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Kurzer Scale-Bounce plus ausklingender Ring in der Aktionsfarbe
(rot für Favorit, grün für Wunschliste), sobald der Button eine
Markierung setzt. Beim Wieder-Abwählen bleibt es ruhig — hilft
die Bestätigung visuell abzusetzen.
Die Animation wird per tick()-Zwischenschritt (false → tick → true)
gestartet, damit mehrfache Klicks innerhalb weniger hundert ms die
Animation neu triggern. prefers-reduced-motion schaltet den Effekt
aus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Das Herz auf der Wunschliste doppelte sich semantisch mit dem
Favoriten-Herz in der Rezeptansicht. Jetzt deckungsgleich mit dem
Wunschlisten-Icon in der Rezept-Action-Bar: Utensils. Aktiv-Farbe
von rot auf das Kochwas-Grün gewechselt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Quote-Pool war bei 51 — mit jedem Start sah man schnell
Wiederholungen. 100 weitere im gleichen augenzwinkernden Ton
hinzugefügt. Alle non-offensive, keiner doppelt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der bisherige immer sichtbare URL-Importbalken ist durch einen
"Rezept hinzufügen"-Button rechts im Register-Head ersetzt. Klick
öffnet ein kleines Dropdown mit zwei Optionen:
• Von URL importieren — öffnet einen Modal-Dialog zur URL-Eingabe
und leitet wie bisher nach /preview weiter.
• Leeres Rezept — POST /api/recipes/blank, Weiterleitung nach
/recipes/{id}?edit=1; die Detailseite erkennt den Param und
startet direkt im Editor, entfernt ihn nach Aktivierung wieder
aus der URL.
Der neue Blank-Endpoint legt ein Rezept mit Platzhalter-Titel
"Neues Rezept", Portions-Default 4 und leeren Listen an. Der User
füllt direkt im Edit-Modus aus und speichert wie gewohnt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Emmi kocht einfach (und viele andere WordPress-Seiten) liefert unter
/favicon.ico ein Hoster-Default — Zahnrad-Artige Grafik — während das
eigentliche Site-Icon nur per <link rel="icon"> im <head> auftaucht.
Jetzt ziehen wir die Icon-Kandidaten erst aus der Homepage, sortieren
nach "sweet spot" 32–192 px und fallen bei Fehlschlag wie bisher auf
/favicon.ico und dann Google s2/favicons zurück.
Migration 011 setzt alle favicon_path=NULL, damit existierende
(falsche) Favicons beim nächsten Start neu geladen werden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Beim Klick auf eine andere Sort-Pille wurde allRecipes=[] eager
gesetzt; dadurch kollabierte der Block unter den Chips und der
Browser snapte nach oben. Jetzt laden wir erst die neuen Treffer,
tauschen dann atomar. Sollte sich die Block-Höhe trotzdem ändern
(z.B. von 50 geladenen Items zurück auf 10), korrigieren wir per
scrollBy-Delta, damit die Chips visuell an Ort bleiben.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Die Mobile-Only-Regel führte auf Desktop zu riesigen 16:10-Covers
(auf 1440px-Breite fast 900px hoch). Jetzt gilt der 30vh-Deckel
überall — Bild bleibt proportional, Zutaten/Zubereitung bleiben
sichtbar ohne Scroll.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DDG erkennt die Pi-IP als Bot und antwortet bei jeder Anfrage mit
CAPTCHA (suspended_time=0, also sofort erneut, aber immer derselbe
Müll). Raus damit. Brave (API, stabil, kein Scraping-Limit) plus
Mojeek (eigener Index) liefern die Web-Treffer — das reicht für den
Kochwas-Scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Startpage lieferte Captcha-Redirects (1h suspended_time pro Fehler) —
bringt für Rezeptsuche gegenüber Brave/DDG keinen Mehrwert.
Gleich mitgenommen: ahmia/torch (brauchen Tor-Proxy, den wir nicht
haben) und wikidata (Cold-Start-KeyError in SearXNG 2026.4). Alle drei
produzierten nur Log-Noise, keine nützlichen Treffer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bisher lief der Dev-SearXNG über ein direktes bind-mount von
./searxng:/etc/searxng, was ${…}-Platzhalter im settings.yml als Literal
übernommen hat (Brave-Key konnte so nicht getestet werden).
Jetzt spiegelt der Dev-Compose das Prod-Pattern:
- searxng-init-Container expandiert die Platzhalter per Python und
legt die gerenderte settings.yml auf ein named volume.
- searxng-Container mountet das volume statt bind.
- depends_on mit service_completed_successfully → sauberes Startup.
- Werte kommen aus .env (BRAVE_API_KEY, SEARXNG_SECRET); Default für
SEARXNG_SECRET bleibt compose-seitig gesetzt, damit man ohne .env
booten kann.
.env.example erweitert um BRAVE_API_KEY und SEARXNG_SECRET, mit kurzen
Kommentaren zu Beschaffung und Erzeugung.
Flow für einen Neu-Einsteiger:
cp .env.example .env # Key optional eintragen
docker compose up -d # bringt SearXNG hoch
npm run dev # startet die App lokal
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der entrypoint-Override im vorherigen Commit scheiterte, weil der
erwartete Pfad /usr/local/searxng/dockerfiles/docker-entrypoint.sh im
aktuellen SearXNG-Image (granian-basiert) nicht existiert. Stattdessen
jetzt ein Ein-Shot-Init-Container mit dem gleichen SearXNG-Image:
- searxng-init: liest ./searxng/settings.yml read-only, expandiert
${VAR}-Platzhalter per Python os.path.expandvars, schreibt Ergebnis
auf ein named volume (searxng-config).
- searxng: mountet searxng-config auf /etc/searxng und startet
unverändert mit seinem Original-Entrypoint (kein Pfad-Raten).
- depends_on mit condition: service_completed_successfully → searxng
wartet auf fertigen Init.
settings.yml: secret_key nutzt ${SEARXNG_SECRET} ohne :- default
(Python-expandvars kennt das nicht). Der Default landet als ENV im
Compose.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🌐/👥/💾 waren die letzten verbliebenen Emoji-Icons in der App und
stachen gegen die Lucide-Icons im Rest heraus. Jetzt Globe/Users/
DatabaseBackup als SVG-Icons in den Admin-Tabs, passt zum übrigen
Design.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Import-von-URL-Form war unter dem Hero-Suchfeld auf der Home-Seite —
dort etwas deplaziert, weil die Home-Seite primär Suche + Listen
zeigt. Jetzt sitzt das Feld oben auf der /recipes-Seite (Register),
dem natürlicheren Ort zum Verwalten der Sammlung. Link-Icon links,
grüner „Importieren"-Button rechts, auf allen Viewport-Größen sichtbar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SearXNG v2026 kennt keinen !env-YAML-Constructor — Container crasht
mit „could not determine a constructor for the tag '!env'". Fix: wir
mounten settings.yml read-only auf /config-src, und ein Entrypoint-Hook
schreibt beim Start eine expandierte Fassung nach /etc/searxng/settings.yml
(mit os.path.expandvars — Python ist im Image, envsubst fehlt).
- settings.yml: api_key nutzt jetzt ${BRAVE_API_KEY} statt !env.
- docker-compose.prod.yml: searxng-Container bekommt entrypoint-
Override, reicht BRAVE_API_KEY + SEARXNG_SECRET als Env durch und
expandiert das YAML vor exec.
Leerer Key ist weiterhin ok — Brave antwortet dann mit 401, andere
Engines bleiben unberührt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>