Commit Graph

180 Commits

Author SHA1 Message Date
hsiegeln
aaaf762564 feat(editor): Zutaten umsortierbar + Zutat/Notiz gleich breit
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m24s
- 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>
2026-04-18 21:32:35 +02:00
hsiegeln
dc04f5b032 feat(recipe): Schrift im Tablet/Desktop-Layout vergrößert
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s
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>
2026-04-18 21:25:17 +02:00
hsiegeln
2f2f7dc7e7 fix(searxng): Mojeek entfernt — blockt die Pi-IP mit 403
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m20s
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>
2026-04-18 21:15:54 +02:00
hsiegeln
76ea5bed8d fix(searxng): nur Brave+Mojeek abfragen, DDG-Captcha-Noise beseitigen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s
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>
2026-04-18 21:10:55 +02:00
hsiegeln
854af2fc34 fix(pwa): Reload-Loop beim Zombie-Cleanup beseitigt
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 29s
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>
2026-04-18 18:12:19 +02:00
hsiegeln
1bec054ec6 fix(pwa): Zombie-waiting-SW via GET_VERSION erkennen (Live-Bug)
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s
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>
2026-04-18 18:06:36 +02:00
hsiegeln
c2074c9768 refactor(pwa): auf Workbox-Standard vereinfacht, refreshing-Flag
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
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>
2026-04-18 17:57:51 +02:00
hsiegeln
858d4c1622 fix(pwa): Zombie-Waiting-SW erkennen und stumm aufräumen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m23s
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>
2026-04-18 17:48:55 +02:00
hsiegeln
42f79f122b fix(api): PATCH akzeptiert servings_default=0
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
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>
2026-04-18 17:30:57 +02:00
hsiegeln
3d6f6393b3 fix(pwa): Endlos-Loop "Neue Version verfügbar" beseitigt
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s
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>
2026-04-18 17:27:04 +02:00
hsiegeln
8bb208a613 feat(pwa): Admin-Tab "App" mit Install + Sync + Cache-Reset
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m20s
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>
2026-04-18 16:57:49 +02:00
hsiegeln
3906781c4e feat(pwa): Schreib-Aktionen zeigen Offline-Toast statt stillem Fail
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
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>
2026-04-18 16:54:03 +02:00
hsiegeln
447ff2be32 fix(pwa): SW-Manifest trackt nur wirklich gecachte Rezepte
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
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>
2026-04-18 16:50:26 +02:00
hsiegeln
51a88a4c58 feat(pwa): SW Pre-Cache-Orchestrator mit Fortschritt + Delta-Sync
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
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>
2026-04-18 16:44:48 +02:00
hsiegeln
582d902c62 feat(pwa): Service-Worker-Gerüst mit Shell-Cache + Fetch-Dispatch
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
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>
2026-04-18 16:38:09 +02:00
hsiegeln
7c8edb9b92 feat(pwa): Cache-Manifest-Diff-Funktion + Tests
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
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>
2026-04-18 16:34:39 +02:00
hsiegeln
d38992661c feat(pwa): Cache-Strategy-Entscheider + Unit-Tests
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Reine TS-Funktion resolveStrategy({url, method}) → 'shell' | 'swr'
| 'images' | 'network-only'. Kernregel:
  - Schreib-Methoden + import/preview/search/web → network-only
  - /images/* → images (cache-first)
  - /_app/, manifest, Icons, favicon, robots → shell (cache-first)
  - Alles andere same-origin-GET → swr
7 Tests decken alle Buckets ab. Wird vom SW in Task 8 aufgerufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:32:30 +02:00
hsiegeln
02df0331b7 feat(pwa): SyncIndicator-Pill mit Overlay-Karte
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
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>
2026-04-18 16:29:31 +02:00
hsiegeln
d08cefa5c9 feat(pwa): Sync-Status-Store mit localStorage-Persistierung
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
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>
2026-04-18 16:25:35 +02:00
hsiegeln
0c66bd677e feat(pwa): Toast-Store + Renderer
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
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>
2026-04-18 16:21:14 +02:00
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
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
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
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