Commit Graph

81 Commits

Author SHA1 Message Date
hsiegeln
e953ca7870 feat(comments): Trash-Button zum Loeschen eigener Kommentare
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
Der DELETE-Endpunkt fuer Kommentare existierte schon, hatte aber
keine UI-Exposition — Nutzer konnten ihre eigenen Kommentare nur
via API-Call loeschen. Das war beim UAT 2026-04-19 aufgefallen.

Jetzt: pro Kommentar wird nur fuer den Autor (comment.profile_id
=== profileStore.active.id) ein kleiner Trash2-Button in der
Ecke angezeigt. Mit confirmAction-Dialog, weil das Loeschen
nicht undo-bar ist.

Nutzt asyncFetch fuer den DELETE-Call — konsistent mit dem
Rest des Page-Scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:56:41 +02:00
hsiegeln
c1789f902e fix(preview): Guard wenn ?url=-Parameter fehlt
/preview ohne Query zeigte endlos "Vorschau wird geladen…", weil
loading initial true war und der $effect bei leerem u nichts tat.

Jetzt: beim leeren u wird errored gesetzt (mit Hinweis, dass das
der falsche Einstieg in die Route ist), so zeigt die bestehende
error-box den passenden Text an.

Im UAT 2026-04-19 aufgefallen, dort als MINOR eingeordnet.
Hier direkt mitgenommen weil 6-Zeilen-Fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:55:18 +02:00
hsiegeln
02b9cdbc68 refactor(client): requireProfile(message?) + Wunschliste migriert (Item G)
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s
Option B aus dem Roadmap-Plan. requireProfile bekommt einen optionalen
message-Parameter mit dem bisherigen Text als Default — die 5 Bestands-
Aufrufe aendern sich nicht, die Wunschliste nutzt die Custom-Message
„um mitzuwünschen" sauber ueber den Helper statt mit dupliziertem
alertAction-Block.

Netto: -3 Zeilen in wishlist/+page.svelte, eine Duplikation weniger,
Helper dokumentiert jetzt explizit den Message-Override-Use-Case.

Gate: svelte-check 0 Warnings, 184/184 Tests, Wunschliste zeigt
korrekte Message beim Klick ohne Profil.

Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item G.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:45:00 +02:00
hsiegeln
5a291a53dd refactor(ui): --pill-radius CSS-Variable (Item F)
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m22s
border-radius: 999px war 15x im CSS dupliziert. Ausgelagert als
:root --pill-radius Variable im globalen :root-Block in +layout.svelte,
Call-Sites auf var(--pill-radius) umgestellt.

Bewusst NICHT angefasst (plan war "nur Werte die mehrfach vorkommen"):
- z-index: 10 Distinct Values in 14 Sites, bilden ein implizites
  Layer-System. Konsolidieren = behavior-change-Risiko ohne konkreten
  Nutzen. Wenn kuenftig einheitliche Modal-/Popover-Layer noetig,
  separate Phase.
- setTimeout(): 3 Sites, jeder mit eigener Semantik (Debounce/Print/
  Spinner). Kein DRY-Nutzen durch Extraktion.

Gate: svelte-check 0 Warnings, 184/184 Tests, Build clean, kein
sichtbarer Unterschied (einzige Aenderung: selber Wert ueber Variable).

Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item F.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:43:19 +02:00
hsiegeln
5a1ffee3bb refactor(editor): untrack() fuer form-lokale Snapshots (Item I)
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
Alle 10 pre-existing svelte-check WARNINGs ("state_referenced_locally")
in RecipeEditor.svelte und recipes/[id]/+page.svelte addressiert.

Die betroffenen `let foo = $state(recipe.bar)`-Pattern sind
intentional Snapshots: der Editor soll User-Edits behalten und nicht
von prop-Updates ueberschrieben werden. untrack() macht die Intent
explizit und silenced die Warnung sauber statt sie unter den Teppich
zu kehren.

Scope: imagePath, title, description, servings, prepMin, cookMin,
totalMin, ingredients, steps (RecipeEditor) + recipeState
(recipes/[id]/+page).

Gate: svelte-check 0 Warnings (war 10), Tests 184/184.

Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item I.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:38:35 +02:00
hsiegeln
31c6e5cd1f refactor(server): IMAGE_DIR/DATABASE_PATH zentralisieren + Doku-Drift fixen
src/lib/server/paths.ts: zentrale Auflösung der env-vars; vorher 6×
IMAGE_DIR und 2× DATABASE_PATH dupliziert mit identischen Defaults.

Migrierte Sites:
- src/lib/server/db/index.ts (DATABASE_PATH + IMAGE_DIR)
- src/routes/api/admin/backup/+server.ts
- src/routes/api/domains/+server.ts
- src/routes/api/domains/[id]/+server.ts
- src/routes/api/recipes/import/+server.ts
- src/routes/api/recipes/[id]/image/+server.ts
- src/routes/images/[filename]/+server.ts

ARCHITECTURE.md:
- 49 Flachwitze -> 150 (waren tatsaechlich 150)
- 'search/' Route entfernt — wurde nie als eigene Route gebaut, Suche
  laeuft direkt auf der Homepage via API-Calls

Findings aus zweiter Review-Runde (siehe OPEN-ISSUES-NEXT.md)
2026-04-18 22:41:02 +02:00
hsiegeln
30a447a3ea refactor(client): requireProfile() + asyncFetch wrapper
requireProfile():
- src/lib/client/profile.svelte.ts: neuer Helper, returnt das aktive
  Profile oder null nach standardisiertem alertAction
- 5x in recipes/[id]/+page.svelte: setRating, toggleFavorite, logCooked,
  addComment, toggleWishlist verlieren je 7 Zeilen Guard-Klausel
- profile-Variable im Closure macht den ! am profileStore.active obsolet

asyncFetch():
- src/lib/client/api-fetch-wrapper.ts: returnt Response auf 2xx, null
  nach alertAction auf Fehler
- 4 Call-Sites umgestellt: saveRecipe + saveTitle (recipes/[id]),
  saveEdit (admin/domains), rename (admin/profiles)
- admin/domains add() bewusst nicht migriert — inline-Error-UX statt Modal

Findings aus REVIEW-2026-04-18.md (Quick-Win 5) und redundancy.md
2026-04-18 22:22:19 +02:00
hsiegeln
ff293e9db8 refactor(api): alle handler auf api-helpers umstellen
13 +server.ts handler nutzen jetzt parsePositiveIntParam und
validateBody statt jeweils lokaler parseId/safeParse-Bloecke.

Konsequenzen:
- 9 lokale parseId/parsePositiveInt Definitionen geloescht
- 11 safeParse + manueller error()-Throws ersetzt
- domains/[id], domains, profiles: catch-Block reicht jetzt HttpError
  durch (isHttpError) — vorher wurde ein 404 vom updateDomain als 409
  re-emittiert
- recipes/[id]/image: kein function-clutter mehr neben den FormData-Pfaden
- Konsistente Error-Bodies: validateBody schickt {message, issues},
  parsePositiveIntParam {message: 'Missing X' / 'Invalid X'}

Findings aus REVIEW-2026-04-18.md (Refactor A) und redundancy.md
2026-04-18 22:19:12 +02:00
hsiegeln
830c740747 refactor(constants): zentrale SW-Timing-Konstanten + minor cleanups
- 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
2026-04-18 22:14:38 +02:00
hsiegeln
5283ab9b51 feat(recipe): Bild manuell hochladen / ersetzen / entfernen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m22s
- 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>
2026-04-18 21:39:54 +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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
d3c9bc5619 feat(cooked): „Heute gekocht" räumt Wunschliste für das Rezept
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Wenn ein Rezept heute gekocht wurde, ist der Wunsch eingelöst — raus
damit aus der Wunschliste aller Profile. Server tut das beim POST in
einem Rutsch (removeFromWishlistForAll) und meldet removed_from_wishlist
in der Response zurück. Der Client räumt daraufhin den lokalen
wishlistProfileIds-State und refresht den Badge-Zähler, damit der
Wunschliste-Button und das Header-Badge sofort passen — kein Reload nötig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 22:23:08 +02:00
hsiegeln
dbc9646caa fix(nav): Hamburger-Menü in Kochwas-Grün statt Schwarz
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
Der Menü-Button ist ein <button>, nicht <a>, und hat deshalb nicht
vom globalen Link-Color geerbt. Farbe jetzt explizit auf #2b6a3d
gesetzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 22:14:21 +02:00
hsiegeln
c27c2dbc62 feat(search): „+ weitere Ergebnisse" auch im Header-Dropdown
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Der Suchfeld-Dropdown auf Rezept-/Preview-Seiten hatte nur bei lokalen
Treffern einen Fuß-Link. Bei reinem Web-Ergebnis fehlte die Weiterführung.
Jetzt steht „+ weitere Ergebnisse" unter jeder Trefferliste und
navigiert auf /?q=, wo die Hauptseite inline weiter paginieren kann.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 22:13:03 +02:00
hsiegeln
1b7c5c084e feat(search): Home als einzige Suchseite, inline „+ weitere Ergebnisse"
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Die separaten /search und /search/web Routen sind weg. Auf der Hauptseite
gibt es jetzt einen einzigen „+ weitere Ergebnisse"-Button am Ende der
Trefferliste, der erst weitere lokale Treffer lädt und — sobald die
erschöpft sind — auf die SearXNG-Web-Suche umschaltet und dort Seite für
Seite weiterblättert. Web-Treffer werden unter die lokalen angehängt,
getrennt durch eine „Aus dem Internet"-Zwischenüberschrift.

Alte Layout-Links auf /search bzw. /search/web zeigen jetzt auf /?q=.

Der Snapshot der Suche merkt sich auch Paginations-Zustand, damit
Rücknavigation vom Rezept/Preview die volle Liste wiederherstellt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 22:08:00 +02:00
hsiegeln
a62b32aa1e feat(search): „+ weitere Ergebnisse"-Button für lokale und Web-Suche
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m20s
Die Ergebnislisten waren oft kurz, weil lokale Suche auf LIMIT 30 und
die Web-Suche auf die erste SearXNG-Seite beschränkt war. Jetzt lässt
sich beides nachladen.

- `searchLocal` nimmt jetzt einen `offset` und der `/api/recipes/search`-
  Endpoint einen `?offset=`-Parameter.
- `searchWeb` nimmt jetzt eine `pageno`-Option und reicht sie als
  `pageno`-Parameter an SearXNG weiter. `pageno=1` wird weggelassen,
  damit bestehendes Verhalten unverändert bleibt.
- `/search` und `/search/web` zeigen unterhalb der Liste einen
  „+ weitere Ergebnisse"-Button. Beide deduplizieren nachgeladene
  Hits (ID bzw. URL), weil SearXNG das gleiche Ergebnis auf zwei
  Seiten liefern kann.

Kein Endless-Scroll: expliziter Button ist mobil robuster und spart
die teure Thumbnail-Enrichment-Roundtrip-Zeit, die bei jeder neuen
Web-Seite anfällt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:58:47 +02:00
hsiegeln
b4a7355b24 feat(nav): Hamburger-Menü mit Register statt Settings-Icon
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
Ersetzt das Settings-Zahnrad im Header durch ein Dreistriche-Menü. Das
Menü enthält zwei Punkte: „Register" führt zu einer neuen /recipes-Route
mit allen Rezepten alphabetisch gruppiert (A-Z-Buchstabenchips zum
Scrollen, Live-Filter oben, Umlaut-normalisiert). „Einstellungen" zeigt
wie bisher /admin.

Auf Mobile <520px wird das App-Icon komplett ausgeblendet, damit die
Suchleiste mehr Platz bekommt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:54:04 +02:00
hsiegeln
f72fe64d8e feat(pwa): Update-Toast zeigt neue Version an
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
pwaStore ($lib/client/pwa.svelte.ts):
- Hängt sich an navigator.serviceWorker.ready, hört auf updatefound und
  setzt updateAvailable = true, sobald ein neuer SW im Status 'installed'
  ist UND es einen aktiven controller gibt (= Update eines bestehenden
  Tabs, nicht die erste Installation).
- Polling alle 30 Minuten via registration.update(), damit der User den
  Toast auch sieht, wenn er die Seite lange offen hat ohne zu navigieren.
- reload() ruft location.reload(); dismiss() schließt den Toast nur.

UpdateToast.svelte:
- Schwarzer Pill-Toast unten zentriert, mit Text, grünem "Neu laden"-
  Button (RefreshCw-Icon) und X zum Wegklicken.
- Slide-Up-Animation beim Erscheinen.
- Responsive: auf Mobile (<420px) wird's zum vollbreiten Banner statt
  Pill.

Root-Layout mountet <UpdateToast /> direkt neben <ConfirmDialog />.
onMount ruft pwaStore.init().

Status-Check der Live-Instanz https://kochwas.siegeln.net:
- manifest.webmanifest wird korrekt als JSON ausgeliefert
- service-worker.js (3.4 KB) ist verfügbar
- iOS Apple-Meta-Tags + Android theme-color sind im HTML <head>
PWA selbst funktioniert also bereits; der Toast war das fehlende Teil
für transparente User-seitige Updates.
2026-04-17 19:38:00 +02:00
hsiegeln
dd52e44f67 fix(ui): Emoji-Reste im Profil-Modal + Wunschliste-Icon → Kochtopf
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Profil-Modal:
- Default-Emoji '🍳' im "Neues Profil"-Input entfernt (war ein ver-
  sehentlicher Platzhalter, den die meisten nicht überschrieben haben
  → alle Profile sahen gleich aus). Jetzt leer, mit 🙂 als Hint im
  placeholder.
- Profil-Liste: avatar_emoji wird nur gezeigt, wenn wirklich gesetzt.
  Sonst CircleUser-Lucide statt 🙂-Fallback.

Migration 006_clear_default_profile_emoji.sql räumt bestehende DB-
Einträge auf: UPDATE profile SET avatar_emoji = NULL WHERE avatar_emoji
= '🍳'. User, die wirklich einen Pfannen-Avatar wollten, können das in
/admin/profiles neu setzen.

Wunschliste-Header-Icon: Heart → CookingPot. Der Kontext ist "was wir
essen wollen", also passt ein Topf besser als ein Herz. Heart bleibt
im Rezept als "Favorit" und in der Wunschliste als "ich will auch"-
Toggle, keine Kollision.

Ungenutzten Heart-Import aus +layout.svelte entfernt.
2026-04-17 19:31:24 +02:00
hsiegeln
5e7e37cc3c feat(wishlist): "für alle löschen" + Badge refresht auf jede Navigation
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m14s
1) Trash-Button auf Wunschliste wieder da. Im Gegensatz zum Heart
   entfernt er den Eintrag NICHT nur für das aktive Profil, sondern
   löscht alle Memberships auf diesem Rezept. Bestätigungsdialog macht
   das explizit ("wird für alle Profile aus der Wunschliste gestrichen").

   - repository.ts: neue Funktion removeFromWishlistForAll(recipeId)
   - DELETE /api/wishlist/:id?all=true → family-wide
     DELETE /api/wishlist/:id?profile_id=X → nur mein Eintrag
   - UI: zwei Action-Buttons untereinander (Heart, Trash)

2) wishlistStore.refresh() läuft jetzt in afterNavigate des Root-Layouts.
   Vorher wurde der Badge nur aktualisiert, wenn derselbe Tab die Aktion
   ausgelöst hat. Wenn ein anderer Tab / anderes Gerät etwas ändert,
   bleibt der Badge stale bis zum nächsten Full-Reload. Mit afterNavigate
   reicht eine Client-Navigation, um ihn zu aktualisieren — was deutlich
   näher an dem liegt, was der User erwartet.
2026-04-17 19:29:00 +02:00
hsiegeln
018fc987cd feat(recipe): Wake-Lock-Schalter + Profil-Chip nur Lucide + Save-Text
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m16s
1) ProfileSwitcher-Chip: Profil-Emoji (avatar_emoji) ist jetzt aus dem
   Header-Badge raus — immer CircleUser-Icon vor dem Namen. Im Profil-
   Auswahl-Modal bleiben die individuellen Emojis erhalten, damit User
   ihr Profil dort weiterhin erkennen. Unused .emoji CSS entfernt.

2) Preview-Button: "In meine Sammlung speichern" → "Rezept in Kochwas
   speichern". Klarer, was die App heißt.

3) Wake-Lock-Schalter:
   - Erklärung: navigator.wakeLock.request('screen') hindert Android/iOS
     daran, das Display zu dimmen/zu sperren, solange der Tab sichtbar
     ist. Beim Kochen sehr nützlich — Hände sind klebrig.
   - Neuer Toggle-Button im Rezept-Detail, zweite Aktion-Zeile zwischen
     "Heute gekocht" und "Löschen": Lightbulb (an, gelb-gehighlighted)
     oder LightbulbOff (aus).
   - Preference wird in localStorage persistiert (kochwas.wakeLock),
     Default an. Gilt für alle Rezepte.
   - visibilitychange-Handler re-requestet den Sentinel, wenn User den
     Tab wieder nach vorne holt und die Pref an ist.
   - release-Event räumt wakeLock-Variable sauber auf.
2026-04-17 19:21:28 +02:00
hsiegeln
60021b879f feat(wishlist): per-user Wünsche + Header-Badge mit Gesamtzahl
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m16s
Schema-Änderung (Migration 005):
- Tabelle wishlist umgestellt auf PK (recipe_id, profile_id)
- wishlist_like-Tabelle zusammengelegt — Liken WAR schon "will ich auch",
  also werden alle bestehenden Likes Memberships auf der neuen Tabelle.
- Alt-Einträge mit added_by_profile_id werden migriert, anonyme gehen
  verloren (war inkonsistent, jetzt erzwingen wir profile_id NOT NULL).

Repository:
- listWishlist aggregiert pro Rezept: wanted_by_count, wanted_by_names
  (kommagetrennt), on_my_wishlist für das aktive Profil
- listWishlistProfileIds(recipeId) für den Recipe-Page-Loader
- countWishlistRecipes für das Header-Badge (DISTINCT recipe_id)
- addToWishlist/removeFromWishlist/isOnMyWishlist alle mit profile_id
  als Pflicht

API:
- POST /api/wishlist: profile_id jetzt Pflicht (nullable raus)
- DELETE /api/wishlist/[recipe_id]?profile_id=X (nur eigenes Entry)
- /api/wishlist/[recipe_id]/like komplett entfernt (Konzept obsolet)
- Neu: GET /api/wishlist/count → { count: <distinct recipes> }

UI:
- Header-Heart bekommt rotes Badge mit Zahl der Wunschliste-Rezepte.
  wishlistStore in $lib/client/wishlist.svelte.ts hält den Count reaktiv;
  Refresh auf Mount, nach Add/Remove, beim Öffnen der Wunschliste.
- Recipe-Detail: Loader liefert wishlist_profile_ids; onMyWishlist ist
  ein $derived. Toggle fragt aktives Profil (alertAction sonst), mutiert
  die lokale Liste + ruft wishlistStore.refresh.
- Wunschliste-Seite: Heart toggelt eigenen Wunsch, Count zeigt Gesamt-
  wünsche, kommagetrennte Namen zeigen "wer will". Trash-Button
  entfernt — Heart-off reicht jetzt.

Tests (99 → 99, 8 neu geschrieben):
- Per-User-Add/Remove, aggregierte Counts, on_my_wishlist, Cascades bei
  Recipe/Profile-Delete, countWishlistRecipes = DISTINCT.
2026-04-17 19:16:19 +02:00