Commit Graph

238 Commits

Author SHA1 Message Date
hsiegeln
04641355df feat(pwa): Online-Status-Store
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m31s
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>
2026-04-18 16:17:11 +02:00
hsiegeln
0b12aa027f feat(pwa): PNG-Icons 192/512 + Manifest maskable-fähig
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m35s
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>
2026-04-18 16:12:40 +02:00
hsiegeln
60f6db9091 docs(plan): v1.1 Offline-PWA Implementierungsplan
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 30s
15 bite-sized Tasks mit exakten Pfaden und Code-Blöcken. Reihenfolge:
Icons/Manifest → Stores (Network/Toast/Sync-Status) → SyncIndicator →
Cache-Strategy/Diff pure Funktionen → SW-Gerüst → Pre-Cache-Orchestrator
→ Schreib-Aktionen mit Offline-Check → Admin-App-Tab → Playwright →
E2E-Tests → Docs-Updates → Final Manual Checks.

TDD wo sinnvoll (Pure Functions + Stores). SW selber manuell getestet
via preview + DevTools, plus Playwright-E2E.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:07:11 +02:00
hsiegeln
303939a6ff docs(spec): v1.1 Offline-PWA Design
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 46s
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>
2026-04-18 15:59:39 +02:00
hsiegeln
2807dd1cab feat(import): manuelle URL-Importe von allen Domains zulassen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m14s
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>
2026-04-18 15:18:46 +02:00
hsiegeln
7233cc3a13 style(wishlist): Chip-Label "Beliebteste" → "Meist gewünscht"
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
"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>
2026-04-18 15:12:22 +02:00
hsiegeln
297281e201 style(wishlist): Sortierung als Pill-Chips wie auf der Startseite
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m16s
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>
2026-04-18 15:10:52 +02:00
hsiegeln
194aee269e feat(recipe): Pulse-Animation beim Aktivieren Favorit/Wunschliste
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
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>
2026-04-18 15:06:15 +02:00
hsiegeln
361164febd style(wishlist): Herz durch Utensils-Icon ersetzt
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>
2026-04-18 15:06:07 +02:00
hsiegeln
8e33b52f66 feat(quotes): 100 weitere Sprüche für die Startseite
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
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>
2026-04-18 14:50:10 +02:00
hsiegeln
60d0cd7659 feat(register): Rezept-hinzufügen-Dropdown mit URL-Import + Manuell
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
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>
2026-04-18 14:40:57 +02:00
hsiegeln
a10ebefb75 fix(favicons): HTML-<link rel=icon>-Parsing vor /favicon.ico
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
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>
2026-04-18 14:34:17 +02:00
hsiegeln
e56c1543d8 fix(home): Sort-Chip-Wechsel behält Scroll-Position
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>
2026-04-18 14:34:08 +02:00
hsiegeln
8c93099d91 fix(recipe): Header-Bild auf allen Viewports max 30vh
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>
2026-04-18 14:34:01 +02:00
hsiegeln
f92ce677f6 fix(searxng): DuckDuckGo deaktiviert — liefert nur noch Captchas
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m22s
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>
2026-04-18 14:21:47 +02:00
hsiegeln
cbf9b94aa3 fix(searxng): Startpage + Tor + Wikidata deaktivieren
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m24s
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>
2026-04-18 14:19:45 +02:00
hsiegeln
7070a83991 feat(dev): docker-compose.yml als vollwertiges Dev-Setup
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m24s
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>
2026-04-18 14:17:46 +02:00
hsiegeln
a2b3c8981c fix(searxng): Init-Container für Env-Substitution
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
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>
2026-04-18 14:15:01 +02:00
hsiegeln
68e27a6868 style(admin): Emoji-Icons durch Lucide-Icons ersetzt
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m20s
🌐/👥/💾 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>
2026-04-18 14:08:57 +02:00
hsiegeln
351434f43d refactor(home): URL-Import verzog ins Register
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
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>
2026-04-18 14:04:11 +02:00
hsiegeln
49d4e60a1c fix(searxng): Env-Substitution über Python statt !env-YAML-Tag
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
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>
2026-04-18 13:56:22 +02:00
553bf4f924 use wildcard tls cert
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
2026-04-18 13:06:14 +02:00
hsiegeln
1b31a8ff1e feat(searxng): Brave via API-Key, robustere Timeouts + Engine-Mix
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
Nach den 403/Too-Many-Requests-Logs des Pi jetzt SearXNG-Setup auf
API-first + höhere Timeouts umgestellt:

- Brave läuft über den API-Key aus BRAVE_API_KEY (via !env in settings.yml
  gelesen). Kein Scraping-Ban-Spam mehr. Key wird im .env auf dem Pi
  gepflegt (nicht im Repo) und ans searxng-Container durchgereicht.
- outgoing.request_timeout 3s → 8s, max_request_timeout → 12s. Pi
  hängt gelegentlich knapp am Default-Limit, lieber warten als 0
  Treffer.
- DuckDuckGo-Timeout einzeln auf 8s, Mojeek als zusätzliche Quelle
  (eigener Index, selten Rate-Limits).
- Video-/News-/Image-Engines explizit disabled (Google/Bing/karmasearch
  videos etc.) — produzieren für Rezeptseiten nur 403-Noise.

docker-compose.prod.yml reicht BRAVE_API_KEY=${BRAVE_API_KEY:-} an den
searxng-Container weiter. Leerer Key ist ok — Brave meldet 401 bei der
ersten Query, andere Engines laufen unbeeindruckt weiter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:03:17 +02:00
hsiegeln
c79cf8657d style(recipe): Header-Bild auf Mobile max 30vh
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Das 16:10-Cover-Bild konnte auf Smartphones im Hochformat locker 40-50%
des Viewports füllen und Aktionen wie Favorit/Wunschliste fast aus dem
ersten Screen drücken. Auf <820px jetzt max-height:30vh — object-fit:
cover sorgt dafür, dass das Bild schön im gekürzten Rahmen sitzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:45:31 +02:00
hsiegeln
9a5c626890 feat(recipe): Edit-Modus für Zutaten, Schritte und Meta
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Auf der Rezept-Detail-Seite ein neuer „Bearbeiten"-Button (Pencil-Icon)
in der Action-Bar. Klick schaltet RecipeView auf RecipeEditor um.

Im Editor:
- Titel, Beschreibung, Portionen, Vorbereitungs-/Koch-/Gesamtzeit als
  inline-Inputs.
- Zutaten: pro Zeile Menge, Einheit, Name, Notiz + Trash-Icon zum
  Entfernen. „+ Zutat hinzufügen"-Dashed-Button am Listenende.
- Schritte: nummerierte Textareas, Trash-Icon, „+ Schritt hinzufügen".
- Mengen akzeptieren Komma- oder Punkt-Dezimalen.
- Empty-Items werden beim Speichern automatisch aussortiert.

Backend:
- Neue Repo-Funktionen updateRecipeMeta(id, patch), replaceIngredients,
  replaceSteps — letztere in einer Transaction mit delete+insert und
  FTS-Refresh.
- PATCH /api/recipes/[id] akzeptiert jetzt zusätzlich description,
  servings_default, servings_unit, prep_time_min, cook_time_min,
  total_time_min, cuisine, category, ingredients[], steps[]. Vorher
  nur title/hidden_from_recent; diese beiden bleiben als
  Kurz-Fall erhalten, damit bestehende Aufrufer unverändert laufen.
- Zod-Schema mit expliziten Grenzen (max-Länge, positive Mengen).

Tests: 3 neue Cases für updateRecipeMeta, replaceIngredients (inkl.
FTS-Update), replaceSteps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:41:10 +02:00
hsiegeln
ee783ff50b feat(home): URL-Import-Shortcut auf Desktop
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Unter dem Hero-Suchfeld steht ab Viewport >=820px eine zweite kleine
Form mit „… oder Rezept-URL direkt einfügen"-Input und grünem
Importieren-Button. Beim Submit springt die App auf /preview?url=, wo
der bestehende Importer JSON-LD/Microdata extrahiert und die Vorschau
zum Bestätigen zeigt. Auf Mobile versteckt (per CSS), damit die
Hero-Area nicht überladen wird.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:37:10 +02:00
hsiegeln
61c1b9558e fix(searxng): nur Text-Engines via categories=general
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Die SearXNG-Logs waren voller 403-Errors von karmasearch (video-engine)
und gelegentlich Brave. Beide gehören nicht zur general-Kategorie und
bringen für Rezeptseiten nichts — sie werden nur noch vom SearXNG-Core
angefragt, weil wir die Kategorie nicht explizit eingrenzen.

categories=general im Query beschränkt jetzt auf Text-Web-Suche; die
problematischen Video-/News-Engines werden gar nicht erst konsultiert,
und die 403-Spam in den Container-Logs verschwindet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:32:11 +02:00
hsiegeln
340ab5e558 fix(home): „Alle Rezepte" Endless-Loop raus, Sort als Chips
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m28s
Der $effect auf allSort trackte implizit auch allRecipes.length und
triggerte damit bei jedem Append einen Reset + Reload — klassischer
Endless-Loop, Liste wurde ständig zurückgesetzt und nie gerendert.
Ersetzt durch einen expliziten setAllSort()-Handler, der nur beim echten
Klick des Users feuert.

Sort-Kontrolle außerdem vom nativen <select> auf App-eigene Pill-Chips
(Name / Bewertung / Zuletzt gekocht / Hinzugefügt) umgebaut —
konsistent zum Admin-Tabbar und Wunschliste-Sortierung.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:19:56 +02:00
hsiegeln
09c0270c64 feat(home): „Alle Rezepte"-Sektion mit Sortierung und Endless-Scroll
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s
Neue Sektion unter „Zuletzt hinzugefügt": sortierbar nach Name,
Bewertung, zuletzt gekocht und Hinzugefügt. Auswahl persistiert in
localStorage (kochwas.allSort).

- Neuer Endpoint GET /api/recipes/all?sort=name&limit=10&offset=0.
- listAllRecipesPaginated(db, sort, limit, offset) im repository:
  NULLS-last-Emulation per CASE für rating/cooked — funktioniert auch
  auf älteren SQLite-Versionen.
- Endless Scroll per IntersectionObserver auf ein Sentinel-Element am
  Listen-Ende (rootMargin 200px, damit schon vor dem harten Rand
  nachgeladen wird). Pagesize 10.
- 4 neue Tests: Name-Sort, Rating-Sort, Cooked-Sort, Pagination-Offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:14:44 +02:00
hsiegeln
a1d91943c6 fix(home): Dismiss-X auf Recent-Karten immer sichtbar
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m16s
Die Hover-only-Anzeige war auf Touch-Devices eh nicht erreichbar, und
auf dem Desktop genauso wenig sinnvoll — der User musste raten wo das
Schließen-Icon ist. Opacity-Toggles und die 640px-Media-Query raus;
der kleine Cloud-weiße Circle-Button steht jetzt dauerhaft in der
rechten oberen Ecke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:10:47 +02:00
hsiegeln
9e471c7bf3 refactor(nav): Pfeil-Icon im Header statt großem Zurück-Pill
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
Der neu eingefügte Admin-Pill war zu prominent für seinen Zweck. Raus
damit. Stattdessen zeigt der Haupt-Header auf allen Nicht-Hauptseiten
links einen ArrowLeft-Icon-Link zur Startseite — platzsparend und
konsistent über alle Sub-Seiten (Admin, Rezept, Preview, Wunschliste…).
Auf der Startseite selbst bleibt das „Kochwas"-Wortmarke stehen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:06:52 +02:00
hsiegeln
82e8371451 feat(admin): „Zurück"-Button im Settings-Tabbar
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
Auf Mobile <520px ist das Kochwas-App-Icon oben links ausgeblendet, und
aus den Settings gab es keinen sichtbaren Weg zur Hauptseite außer der
Browser-Zurück-Taste. Jetzt steht links vor den Admin-Tabs ein
ArrowLeft-Pill-Button, der direkt auf / führt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:02:43 +02:00
hsiegeln
a8fdb8c3f9 feat(recipe): Zwei-Spalten-Layout ab Tablet-Querformat
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m16s
Auf Viewports >=820px (Tablets im Querformat, Desktop) werden Zutaten
und Zubereitung nebeneinander angezeigt statt per Tab gewechselt. Der
Nutzer sieht beides gleichzeitig und nutzt die volle Display-Breite.

- Tabs bleiben für <820px (Smartphones + Tablet-Hochkant), dort schalten
  sie weiterhin zwischen den Panels um.
- Ab 820px: Tabs versteckt, Grid minmax(260px,1fr) 1.6fr. Zutaten links
  sticky top:1rem mit max-height 100vh-2rem, damit die Liste beim
  Scrollen der Zubereitung sichtbar bleibt.
- Main-Container max-width 760px → 1040px erhöht, damit auf großen
  Screens überhaupt Platz für zwei Spalten bleibt. Andere Listen-
  Ansichten (Cards, Index) nutzen den Zugewinn automatisch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:00:48 +02:00
hsiegeln
3e41505b81 fix(filter): Liste schließt nur noch bei OK/Abbrechen/Escape
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m16s
Der Klick-außerhalb-Handler fing gelegentlich Events von abgewählten
Checkbox-Zeilen ab, weil deren Layout beim Re-Render kurz verschob und
event.target außerhalb des Containers landete — das Menu schloss sich
dann mitten im Filtern. Handler komplett entfernt; Escape und die
expliziten Footer-Buttons bleiben die einzigen Wege zum Schließen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:51:56 +02:00
hsiegeln
4465744838 style(search): Header-Suche angeglichen an Home-Suche
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
Die Header-Suche war Pill-förmig mit grünlichem Hintergrund, die Home-
Suche ein abgerundetes Rechteck mit weißem Hintergrund. Jetzt beide:
white background, border-radius: 12px, gleicher Border, gleicher Fokus-
Outline. Nur die Höhe bleibt unterschiedlich (40px Header vs. 52px
Home), damit der Header kompakt bleibt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 09:06:22 +02:00
hsiegeln
3e3afc0102 fix(importer): Microdata-Steps bei HowToSection + mehrfach-Schritten
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
Rezeptwelt lieferte Zubereitungs-Steps immer als einen einzigen Treffer,
oft mit vermischtem Icon-alt-Text. Zwei Ursachen, beide in der
generischen Microdata-Logik — kein rezeptwelt-spezifischer Parser nötig.

1. HowToSection wrappt HowToSteps als itemListElement, unser Parser sah
   nur das erste. Jetzt: recipeInstructions-Container mit itemtype=
   HowToSection werden abgestiegen, jedes itemListElement wird ein Step.

2. Ein einzelner HowToStep kann intern "1. …<br>2. …<br>3. …" enthalten.
   Neuer textWithLineBreaks(el) konvertiert <br>/Block-Grenzen zu \n und
   ignoriert <img>/<script>/<style>. splitStepText(raw) erkennt
   nummerierte Zeilen und erzeugt einen eigenen Step pro Nummer; Fort-
   setzungszeilen ohne Nummer hängen an den aktuellen Step an.

3 neue Tests: HowToSection-Kette, inline-nummerierter Multi-Step,
<img>-alt-Unterdrückung.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 09:03:50 +02:00
hsiegeln
272935034d fix(filter): Trigger streckt sich auf volle Container-Höhe
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
Der .wrap-Div zwischen .search-box und .trigger war display:block, was
align-self:stretch am Trigger komplett neutralisiert hat — Button war
nur 16px hoch in einem 52px-Container. Playwright hat das klar gezeigt.

- .wrap:has(.trigger.inline) wird jetzt zu display:flex, damit der
  Trigger darin überhaupt ein Flex-Item ist.
- .trigger.inline bekommt height:100% statt sich auf align-self zu
  verlassen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:59:21 +02:00
hsiegeln
c87196cd67 feat(header): Filter-Dropdown auch in der Header-Suche
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
Der SearchFilter-Store galt schon global, aber das UI zum Ändern gab es
nur auf der Home-Seite — auf Rezept-/Preview-Seiten konnte man den
Filter nicht sehen, geschweige denn setzen. Jetzt zeigt die Header-Suche
denselben inline-Trigger links vom Input. Nav-Form bekommt Border +
Background als Container, Input wird transparent, Fokus-Outline landet
auf dem Container (wie auf der Home-Seite).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:53:54 +02:00
hsiegeln
aad3ad689d feat(importer): Microdata-Fallback für Seiten ohne JSON-LD
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
Bisher scheiterte der Import auf Seiten wie rezeptwelt.de mit „Diese Seite
enthält kein Rezept", obwohl unser Such-Filter die Treffer durchließ
(Microdata wird seit dem vorherigen Commit erkannt). Jetzt kann der
Importer die Daten auch tatsächlich extrahieren:

- extractRecipeFromMicrodata(html): parst [itemtype=schema.org/Recipe]-
  Scopes per linkedom, sammelt itemprop-Werte unter Beachtung der
  verschachtelten itemscope-Grenzen (HowToStep-Texts landen nicht im
  Haupt-Scope).
- Übernimmt Content-Attribute auf <meta>/<time> (z.B. prepTime="PT20M"),
  src auf <img>, textContent als Fallback — die Standard-Microdata-
  Value-Regeln.
- Behandelt HowToStep-Items UND einfache <li>/<ol>-Listen als
  recipeInstructions.
- extractRecipeFromHtml ruft JSON-LD zuerst, fällt nur bei null auf
  Microdata zurück — damit bleibt bestehendes Verhalten stabil.

Tests: Königsberger-Klopse-Fixture mit HowToSteps, einfache ol/li-
Variante und Priorität-JSON-LD-über-Microdata-Check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:52:00 +02:00
hsiegeln
ab2acb6437 fix(filter): Hover füllt ganzen Button, Abbrechen links / OK rechts, Alle+Keine
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
- Trigger nimmt volle Höhe des .search-box-Containers ein
  (align-self: stretch, min-height: 0), damit der Hover-Hintergrund
  bündig ausgefüllt wird statt nur innerhalb eines 44px-Rechtecks.
- Footer: justify-content: space-between — Abbrechen sitzt jetzt links,
  OK rechts (übliche Platform-Konvention).
- Quick-Actions: zusätzlicher „Keine"-Button neben „Alle", beide setzen
  den Draft-State ohne sofortigen Commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:49:57 +02:00
hsiegeln
d1ddd51da1 fix(filter): Counter-Badge aus dem Trigger entfernt
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
Der „2/5"-Badge neben dem Slider-Icon war visuell laut und wiederholte
nur, was der User im Dropdown direkt sieht. Raus damit — Trigger zeigt
jetzt nur noch das Icon und den Chevron.

Ungenutzten label-Getter aus dem Store auch entfernt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:43:35 +02:00
hsiegeln
15442ff72b fix(filter): overflow:hidden auf .search-box clippte den Dropdown
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m14s
Der Filter-Button war klickbar, aber das Dropdown-Menu blieb unsichtbar,
weil der umgebende .search-box-Container overflow:hidden hatte (ehemals
wegen Rounded-Corners). Entfernt — da Input und Filter-Trigger eh keinen
eigenen Background haben, ist die Rundung auch ohne Clip sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:42:17 +02:00
hsiegeln
52858f94fe feat(filter): Draft-Auswahl mit OK/Abbrechen-Buttons
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
Der Filter-Dropdown sammelt Checkbox-Klicks jetzt nur noch lokal und
wendet sie erst beim „OK"-Klick auf den Store an. Solange der User
herumklickt, läuft die aktive Suche unverändert weiter. Abbrechen (per
Button, Klick außerhalb oder Escape) verwirft die Draft-Auswahl.

- Neuer searchFilterStore.commit(Set) für One-Shot-Apply (triggert den
  active-$effect nur ein einziges Mal).
- „Alle"-Quick-Action setzt draft = alle Domains explizit; erst beim
  Commit wird das wieder in die leere Menge überführt, damit neu
  freigeschaltete Admin-Domains weiterhin automatisch dabei sind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:34:43 +02:00
hsiegeln
2e196b4834 feat(search): Microdata-Fallback erkennt rezeptwelt & Co.
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
Aus dem Log (q="Königsberger klopse"): 11 rezeptwelt-Treffer kamen durch
alle URL-Filter, wurden aber von hasRecipeJsonLd als non-recipe gedroppt.
Ursache: rezeptwelt.de nutzt Microdata (itemtype=schema.org/Recipe) statt
application/ld+json.

- hasRecipeJsonLd → hasRecipeMarkup: prüft jetzt zusätzlich per Regex
  auf itemtype=(https?://)schema.org/Recipe. Alter Export bleibt als
  Deprecated-Weiterleitung erhalten.
- Log zeigt jetzt auch die ersten 3 gedropten URLs als dropped samples,
  damit neue Problem-Domains einfach zu diagnostizieren sind.
- Migration 010 räumt alle thumbnail_cache-Einträge mit has_recipe=0 aus
  — die waren mit dem alten Check falsch-negativ und müssen neu
  klassifiziert werden.

Tests: 4 neue Cases für hasRecipeMarkup (JSON-LD, http/https Microdata,
Negativ-Fall).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:32:18 +02:00
hsiegeln
15c15c8494 feat(domains): Inline-Edit + Favicon in Settings + Filter IN Suchmaske
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
Domain-Admin-Seite bekommt jetzt ein Favicon-Icon vor jedem Eintrag,
einen Pencil-Button zum Inline-Editieren von Domain und Anzeigename,
und Save/Cancel-Buttons. Beim Ändern des Domain-Namens wird das Favicon
zurückgesetzt und beim Speichern frisch nachgeladen (den Filter-Dropdown-
Icons reicht der neue favicon_path automatisch zu).

Der Filter-Button auf der Hauptseite sitzt jetzt IM weißen Suchfeld-
Container (neuer .search-box-Wrapper mit Border) statt daneben, analog
zum Referenz-Screenshot von rezeptwelt.de. Neue inline-Prop an
SearchFilter schaltet eigenen Border/Background ab und setzt stattdessen
einen vertikalen Divider nach rechts.

- Neuer PATCH /api/domains/[id] mit zod-Schema.
- Repository: updateDomain(id, patch) + getDomainById(id).
  domain-Change nullt favicon_path → Caller lädt neu.
- Tests für updateDomain-Fälle und getDomainById.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:28:02 +02:00
hsiegeln
6c2b24d060 feat(searxng): Suche-Pipeline loggen für Diagnose
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
Statt eine separate Debug-Seite zu bauen: bei jeder Web-Suche werden
zwei kompakte Log-Zeilen nach stdout geschrieben, die den Filter-Verlust
pro Pipeline-Schritt zeigen. In den Pi-Docker-Logs (docker compose logs
kochwas) leicht über grep '[searxng]' zu finden.

Format:
[searxng] q="…" pageno=1 domains=3 raw=12 non_whitelist=2
         non_recipe_url=4 dup=0 kept_pre_enrich=6
[searxng] q="…" pageno=1 enrich=6 dropped_non_recipe=3 final=3

Damit lässt sich gezielt sehen, ob rezeptwelt-Treffer am looksLikeRecipePage-
Filter, am hasRecipe-Check oder schon bei SearXNG selbst verloren gehen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:20:35 +02:00
hsiegeln
a590cf0a57 feat(domains): Favicons laden und im Filter anzeigen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m16s
Für jede Whitelist-Domain wird das Favicon jetzt einmalig geladen und
im image-Verzeichnis abgelegt. SearchFilter zeigt das Icon neben dem
Domain-Namen im Filter-Dropdown.

- Migration 009: allowed_domain.favicon_path (NULL = noch nicht geladen).
- Neues Modul $lib/server/domains/favicons.ts:
  fetchAndStoreFavicon(domain, imageDir) + ensureFavicons(db, imageDir)
  für Bulk-Nachzug; 8 parallele Worker mit 3s-Timeout.
- Reihenfolge: erst /favicon.ico der Domain, Fallback Google-Service.
- GET /api/domains zieht fehlende Favicons auf Abruf nach;
  POST /api/domains lädt direkt im selben Call.
- .ico + .svg jetzt in der /images/[filename]-Route erlaubt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:17:44 +02:00
hsiegeln
d004430854 feat(search): Domain-Filter als Dropdown im Suchfeld
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Links im großen Suchfeld ein Slider-Icon mit Badge („Alle" oder „2/5"),
das ein Dropdown-Menü mit allen Whitelist-Domains als Checkboxen öffnet.
Auswahl wird per localStorage persistiert und gilt global — Header-Such-
Dropdown konsumiert den gleichen Store und sendet den domains-Parameter
bei jedem Fetch mit.

Leere Menge heißt „alle aktiv", damit neu vom Admin freigeschaltete
Domains automatisch dabei sind. Aktive Auswahl landet als explizite
Intersection mit der Whitelist serverseitig.

- searchLocal nimmt jetzt optional string[] domains → `source_domain IN (…)`.
- searchWeb nimmt jetzt opts.domains → site:-Filter auf die Auswahl
  eingeschränkt. Nicht-Whitelist-Einträge werden ignoriert.
- API-Endpoints: `?domains=a.de,b.de`.
- Neuer Client-Store $lib/client/search-filter.svelte.ts.
- Neue Komponente $lib/components/SearchFilter.svelte (mobile-tauglich,
  44px Touch-Targets, Badge auf engen Screens versteckt).

Home-Seite re-runt die Suche bei Filter-Änderung automatisch (150ms debounce),
ohne dass der User neu tippen muss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:13:33 +02:00
hsiegeln
864d113082 feat(header-search): „+ weitere Ergebnisse" lädt inline im Dropdown
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s
Vorher war der „+ weitere"-Link im Header-Dropdown ein Navigations-Link
auf /?q=. Jetzt blättert der Button stattdessen im offenen Dropdown
direkt nach — erst weitere lokale Treffer, dann (wenn lokal erschöpft)
SearXNG-Seiten. Lokale und Web-Treffer werden beide im Dropdown
angezeigt, getrennt durch „Aus dem Internet"-Zwischenüberschrift.

Identische Logik wie auf der Home-Seite, nur im Dropdown-Scope. Dedup
per ID (lokal) bzw. URL (web) gegen SearXNG-Doppler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 08:03:52 +02:00
hsiegeln
0992e51a5d fix(search): Filter zuverlässiger durch allowTruncate
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m16s
Vorher warf fetchText einen Fehler, sobald eine Seite >512 KB war —
bei modernen Rezeptseiten (eingebettete Bundles, base64-Bilder) läuft
das praktisch immer voll. Der Catch-Block hat dann hasRecipe auf NULL
gelassen, und der Treffer ging ungefiltert durch.

Neue FetchOptions.allowTruncate: true → wir bekommen die ersten 512 KB
(das reicht für <head> mit og:image und JSON-LD) statt eines Throws.
Timeout auf 8s erhöht, weil der Pi manchmal langsamer ist.

Migration 008 räumt alte NULL-has_recipe-Einträge aus dem Cache, damit
sie beim nächsten Search frisch klassifiziert werden statt weitere
30 Tage falsch gecached zu bleiben.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 22:33:55 +02:00