Commit Graph

93 Commits

Author SHA1 Message Date
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
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
342ea0efc8 feat(search): Treffer ohne Recipe-JSON-LD rausfiltern
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s
Wir fetchen die Trefferseite sowieso schon fürs Thumbnail — prüfen
jetzt in der gleichen HTML-Parse-Runde, ob überhaupt ein
schema.org/Recipe JSON-LD vorhanden ist. Fehlt es, wird der Treffer
aus der Liste entfernt, weil der Importer auf dieser Seite später
sowieso mit „Diese Seite enthält kein Rezept" scheitern würde.

- Migration 007: thumbnail_cache.has_recipe (NULL=unbekannt, 0=nein, 1=ja).
- Fetch-Fehler hinterlassen NULL → Treffer bleibt konservativ sichtbar.
- Neue export `hasRecipeJsonLd(html)` in json-ld-recipe.ts.
- Alle Cache-Reads/Writes nehmen den neuen Wert mit.

Tests: +2 für Filter/Failover, bestehende Thumbnail-Tests mit
Recipe-JSON-LD-Stub ergänzt, damit sie nicht selber rausgefiltert
werden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 22:20:22 +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
hsiegeln
224352d051 feat(profile): CircleUser-Icon statt 👤 im Profil-Chip
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
2026-04-17 19:08:35 +02:00
hsiegeln
8db67bd1a5 feat(home): SvelteKit snapshot für echte State-Preservation beim Back
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m16s
Vorher (Commit 1055a67): Query in URL gespiegelt, Back triggerte einen
frischen Request — zwar schnell, aber Scroll-Position + Ergebnisreihen-
folge nicht garantiert konstant.

Jetzt: SvelteKit's snapshot-API speichert query + hits + webHits +
searchedFor + webError in der History-Entry. Beim Zurück-Navigieren aus
/preview oder /recipes/[id] stellt restore() den exakten UI-Zustand
wieder her — ohne Fetch. Scroll-Position wird von SvelteKit ohnehin auto-
restored.

Der Debounce-Effekt hat jetzt einen skipNextSearch-Flag, der beim
Restore true gesetzt wird, damit das Setzen von query keinen neuen
Search-Request auslöst. Erstmaliges Tippen nach dem Restore arbeitet
wieder ganz normal mit Debounce.

Die URL-Synchronisation (?q=…) bleibt bestehen — für Bookmarks und
Share-Links. Snapshot überlagert den URL-Load in der Ordnung:
restore → onMount sieht query bereits korrekt gesetzt → kein Double-Work.
2026-04-17 19:06:58 +02:00
hsiegeln
1055a670da fix(preview): Save-Icon als Lucide, Query in URL persistent
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
1. "💾 In meine Sammlung speichern" → Lucide BookmarkPlus + Text.
   Letzter Emoji auf den wichtigen UI-Flächen ist damit weg.

2. Query auf der Startseite wird per history.replaceState nach ?q=…
   in die URL gespiegelt. Folge:
   - User tippt "Pizza" → Ergebnisse erscheinen
   - Klick auf Vorschau pusht neue History-Entry /preview?url=…
   - "Zurück" landet wieder auf /?q=Pizza
   - onMount liest q aus URL, setzt query, Debounce-Effekt feuert,
     Ergebnisse sind wieder da.
   replaceState statt pushState beim Tippen → keine History-Spam-Einträge.
2026-04-17 19:03:50 +02:00
hsiegeln
7cac02de5a feat(ui): Favoriten-Liste, Dismiss-from-Recent, Inline-Rename, Lucide-Icons
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m31s
Homepage:
- Neue Sektion "Deine Favoriten" über "Zuletzt hinzugefügt" (alphabetisch
  sortiert, lädt wenn Profil aktiv ist; versteckt sonst)
- Jede Karte in "Zuletzt hinzugefügt" hat jetzt oben-rechts ein X-Icon
  zum Ausblenden. Das Rezept selbst bleibt in der DB — nur die
  Anzeige in der Recent-Liste wird per recipe.hidden_from_recent = 1
  unterdrückt. Section versteckt sich, wenn die Liste leer wird.

DB:
- Neue Migration 004_recipe_hidden_from_recent.sql (+Index)
- listFavoritesForProfile in search-local.ts (ORDER BY title NOCASE)
- setRecipeHiddenFromRecent in actions.ts

API:
- GET /api/recipes/favorites?profile_id=X
- PATCH /api/recipes/[id] akzeptiert jetzt title und/oder
  hidden_from_recent (Zod-Schema mit refine)

Rezept-Detail:
- Titel ist jetzt inline editierbar: kleines Stift-Icon rechts neben
  H1. Click öffnet Input, Enter speichert (PATCH), Escape bricht ab.
  Kein location.reload() mehr.
- RecipeView bekommt neuen Snippet-Prop titleSlot für Title-Override.
- Neue Aktionsreihenfolge:
  Zeile 1: Favorit | Wunschliste | Drucken
  Zeile 2: Heute gekocht | Löschen
  (Umbenennen ist jetzt am Titel statt in der Leiste.)

Icons (lucide-svelte, neues Dep):
- Emoji-Icons durch Lucide-SVGs ersetzt auf Startseite, Header,
  Rezept-Detail, Wunschliste, Header-Dropdown:
    🍽️→Heart/Utensils, ⚙️→Settings, 🥘→CookingPot, 🌐→Globe,
    ♥/♡→Heart(filled), 🖨→Printer, ✎→Pencil, 🗑→Trash2, ✓→Check,
    🍳→ChefHat, X→X
- Header-Brand-Badge auf Mobile behält sein 🍳 (ist im ::after-Pseudo,
  Lucide käme da nicht sauber rein).
- SearchLoader-Emojis bleiben — die sind Teil der Animations-Charme.

Tests: 99/99 grün (bestehend), Typecheck 0 Fehler.
2026-04-17 18:57:17 +02:00
hsiegeln
657d006441 fix(recipe): Favoriten-Markierung persistiert beim Neuladen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 50s
Bug: Beim Neuanzeigen einer Rezeptseite war der Favoriten-Button immer
grau — isFav wurde als local $state(false) initialisiert und die
checkFavorite()-Funktion war eine Stub-Implementation, die nichts
gemacht hat. State lebte nur innerhalb einer Session.

Fix:
- Neue Server-Funktion listFavoriteProfiles(db, recipeId): number[]
  in $lib/server/recipes/actions.ts
- +page.server.ts lädt favorite_profile_ids mit in die Page-Daten
- +page.svelte macht isFav zum $derived aus favoriteProfileIds +
  aktivem Profil. toggleFavorite mutiert die lokale Liste (Add/Remove
  der aktiven Profil-ID) — beim nächsten Load ist die Server-Liste
  wieder Source of Truth.
- Alte Stub-Funktion checkFavorite() entfernt (inkl. Aufruf in
  onMount).
2026-04-17 18:43:38 +02:00
hsiegeln
cf31e79fb0 feat(loader): SearchLoader mit wackelnder Pfanne und rotierenden Sprüchen
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 54s
Neue Komponente src/lib/components/SearchLoader.svelte ersetzt die
stumpfen "Suche läuft …"-Zeilen an allen vier Stellen:
- Startseite (scope=local und scope=web)
- Header-Dropdown (size=sm, beide Scopes)

Was passiert:
- Ein Pfannen-Emoji (🍳🥘🍲🍜🥣) wechselt alle 900 ms
- Wobble-Animation kippt es im 1.4-s-Takt hin und her
- Drei Dampf-Punkte steigen zeitversetzt auf und fadeen
- Caption unten rotiert alle 1.8 s durch vier passende Sprüche
  (lokal: "Stöbere im Rezeptbuch …", web: "Schnuppere in fremden
  Küchen …" etc.)

Zwei Size-Varianten: md (Homepage) und sm (Header-Dropdown).
2026-04-17 18:40:38 +02:00
hsiegeln
347b1de555 docs: CLAUDE.md + Architecture + Operations für Session-Fortsetzung
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
Drei fokussierte Dokumente, damit eine frische Claude-Session direkt
weiterarbeiten kann, ohne den gesamten Session-Kontext zu brauchen:

- CLAUDE.md (Root): "read me first" — Gotchas, Workflow, No-Gos,
  Quickstart, Verweise auf die Tiefen-Docs.
- docs/ARCHITECTURE.md: Stack, Verzeichnisbaum, Datenfluss (Import,
  Web-Suche, Confirm/Alert), Design-Entscheidungen, Test-Strategie.
- docs/OPERATIONS.md: Deployment-Topologie (Cloudflare → Pi →
  Traefik → kochwas), Gitea-CI-Pipeline, Traefik-Labels mit
  Wildcard-Cert, Troubleshooting (TLS, SearXNG-403, Healthcheck,
  Thumbnail-Cache leeren, Backup), Env-Vars.

Die bestehenden Specs und Plans unter docs/superpowers/ bleiben
unangetastet — die sind Planungs-Artefakte, nicht Betriebsdoku.
2026-04-17 18:38:00 +02:00
hsiegeln
4d90d51501 feat(search): persistenter Thumbnail-Cache in SQLite, Default-TTL 30 Tage
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 54s
Vorher: In-Memory-Map, TTL 30 Minuten. Container-Neustart verwarf den
kompletten Cache, also musste nach jedem Deploy jede Suche wieder alle
Seiten laden.

Jetzt:
- Neue Tabelle thumbnail_cache (url PK, image, expires_at)
- Default-TTL 30 Tage, per Env KOCHWAS_THUMB_TTL_DAYS konfigurierbar
  (7, 365, was der User will — is alles ok laut Nutzer)
- Negative Cache: Seiten ohne Bild werden mit image=NULL gespeichert,
  damit wir nicht jede Suche die gleiche kaputte Seite wieder laden
- Lazy-Cleanup: pro searchWeb-Aufruf werden abgelaufene Zeilen via
  DELETE ... WHERE expires_at <= now() weggeräumt (Index-Scan, billig)

Migration 003_thumbnail_cache.sql: nicht-destruktiv, nur neue Tabelle.
Bestehende DB bekommt sie beim nächsten Start automatisch dazu.

Tests (99/99):
- Neuer Cache-Test: zweiter searchWeb für dieselbe URL macht keinen
  Page-Fetch mehr und liest die image-Spalte aus SQLite.
2026-04-17 18:34:29 +02:00
hsiegeln
1712263fd1 feat(search): HQ-Thumbnails durch immer aktive og:image-Extraktion
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 54s
Vorher: nur Treffer ohne SearXNG-Thumbnail wurden mit dem Seiten-Bild
angereichert. Treffer mit Thumbnail behielten das kleine 150-200 px-
Bildchen aus dem Such-Engine-Index.

Jetzt: Alle Treffer durchlaufen die Enrichment-Pipeline. Wenn die Seite
ein og:image/JSON-LD/Content-Bild hat (und das hat sie bei Rezept-Seiten
praktisch immer), wird das kleine SearXNG-Thumbnail damit überschrieben.
Wenn die Seite kein Bild liefert, bleibt das SearXNG-Thumbnail als
Fallback erhalten.

Das ist das gleiche Bild, das auch die Vorschau anzeigt — Suchergebnis
und Vorschau sind jetzt visuell konsistent.

Performance: Pro erster Suche bis zu ~6 Sekunden zusätzliche Latenz
(max 6 parallel, je 4 s Timeout). Der 30-min In-Memory-Cache macht
Wiederholsuchen instant.

Tests (98/98):
- Neu: SearXNG-Thumbnail wird durch og:image ersetzt.
- Neu: SearXNG-Thumbnail bleibt erhalten, wenn Seite kein Bild hat.
- Alt ("leaves existing thumbnails untouched") entfernt — Verhalten
  hat sich bewusst umgekehrt.
2026-04-17 18:31:42 +02:00
hsiegeln
53e4815508 chore(quotes): Ex-Spruch durch Mikrowellen-Pointe ersetzt
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 50s
2026-04-17 18:07:32 +02:00
hsiegeln
211d58ebec feat(search): Enter bleibt auf Seite + robustere Thumbnail-Erkennung
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 55s
Startseite:
- Enter/Return löst die Suche jetzt sofort aus (cancelt den Debounce),
  navigiert aber NICHT mehr auf /search. Der Anwender bleibt auf der
  gleichen Seite mit Inline-Ergebnissen.

Thumbnail-Enrichment (searxng.ts):
- Regex-basierte og:image-Extraktion durch linkedom-parseHTML ersetzt.
- Neue Fallback-Kette (in dieser Reihenfolge):
    1. <meta property/name = og:image | og:image:url | og:image:secure_url
                           | twitter:image | twitter:image:src>
    2. <link rel="image_src" href="...">
    3. JSON-LD image (auch tief in @graph; "image" als String, Array,
       Objekt-mit-url)
    4. Erstes <img> in article/main/.entry-content/.post-content/figure
- Relative URLs werden gegen die Seiten-URL zu absoluten aufgelöst
  (z.B. /uploads/foo.jpg → http://host/uploads/foo.jpg).
- maxBytes von 256 KB auf 512 KB angehoben, damit JSON-LD-lastige
  Recipe-Seiten nicht mitten im Script abgeschnitten werden.

Tests (97/97):
- Neu: JSON-LD-Image-Fallback-Test.
- Neu: Content-<img>-Fallback-Test mit relativer URL, die zur
  absoluten aufgelöst wird.
2026-04-17 18:04:59 +02:00
hsiegeln
9bc4465061 feat(home): zufälliger Spruch zwischen Titel und Suche
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 50s
Eine der 49 Flachwitze wird beim Laden der Startseite zufällig gewählt
und in kursiv unter "Kochwas" angezeigt. Die Auswahl passiert auf dem
Client (onMount), damit SSR und Hydration nicht miteinander streiten —
beim ersten Frame ist ein nicht-umbrechender Leerraum drin, damit das
Layout nicht springt.
2026-04-17 17:58:27 +02:00
hsiegeln
6a784488f5 fix(search): enrich missing SearXNG thumbnails with og:image
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 55s
SearXNG liefert je nach Seite mal ein thumbnail/img_src mit, mal nicht —
bei Chefkoch-Treffern hatten deshalb zufällig die Hälfte der Kacheln
einen Platzhalter, obwohl die Vorschau dann sehr wohl ein Bild fand.

searchWeb() holt jetzt für jeden Treffer ohne Thumbnail parallel
(max. 6 gleichzeitig, 4 s Timeout pro Request) die Seite und extrahiert
das og:image- oder twitter:image-Meta-Tag. Ergebnis wird 30 min
in-memory gecacht, damit wiederholte Suchen nicht wieder die gleichen
Seiten laden.

Tests:
- Neuer Test: Treffer ohne Thumbnail wird via og:image angereichert.
- Neuer Test: Treffer mit Thumbnail bleibt unverändert (keine Fetch).
- Bestehende Tests deaktivieren Enrichment via enrichThumbnails:false,
  damit sie keine echten Chefkoch-URLs aufrufen.
2026-04-17 17:55:53 +02:00
hsiegeln
3cd22544d3 feat(search): mobile header search expands on focus; drop hero button
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 52s
Home:
- "Suchen"-Button entfernt. Die Suche feuert ohnehin debounced beim
  Tippen; der Button war ein Relikt aus dem Submit-Modell. Enter auf
  dem Input löst weiterhin einen Submit aus (geht zur /search-Seite).

Header (< 520 px):
- Sobald das Suchfeld fokussiert wird, wandert das nav-search-wrap
  via :focus-within auf position: absolute und dehnt sich bis zum
  rechten Rand (1 rem Abstand) aus. Die Action-Icons werden dabei
  vom Suchfeld überlagert (z-index 60), sodass der Anwender auf
  engen Displays deutlich mehr Platz zum Tippen hat.
- Bar-Inner bekam position: relative, damit das absolute Ausdehnen
  innerhalb der Header-Zeile greift.
2026-04-17 17:52:51 +02:00
hsiegeln
d693cb422d feat(search): auto web search when no local hits, offer link otherwise
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 55s
Homepage (/):
- Keine lokalen Treffer → automatisch die Internet-Suche auslösen und
  die Ergebnisse als Karten unterhalb der Suche anzeigen.
- Mindestens ein lokaler Treffer → Karten zeigen + darunter ein
  dezenter Link "🌐 Im Internet weitersuchen" (geht zur /search/web
  Vollseite), keine automatische Internet-Suche.

Header-Dropdown (auf Rezept- und Vorschau-Seiten):
- Gleiche Logik: lokale Treffer oben + Fuß-Link; keine lokalen
  Treffer → Internet-Ergebnisse werden direkt im Dropdown angezeigt.
- Abschnittsüberschrift "Keine lokalen Rezepte – aus dem Internet:"
  trennt den Fallback visuell ab.

Race-Safety bleibt bestehen: Query-Vergleich vor jedem State-Write,
sodass spät ankommende Antworten keinen neueren Suchstand überschreiben.
2026-04-17 17:47:26 +02:00
hsiegeln
76110f9841 fix(nav): right-align action icons even when search is hidden
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
Auf Nicht-Rezept-Seiten (Home, Wishlist, Admin) ist das Header-Suchfeld
ausgeblendet. Ohne flex-Spacer rutschten 🍽️/⚙️/Profil direkt neben das
Brand-Badge — besonders auffällig im Mobile-Layout.

margin-left: auto auf .bar-right schiebt die Action-Icons immer an den
rechten Rand, unabhängig davon ob das Suchfeld sichtbar ist.
2026-04-17 17:43:08 +02:00
hsiegeln
d737618312 feat(search): live debounced search with inline hits and header dropdown
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
Homepage (/):
- Tippen > 3 Zeichen + 300 ms Debounce → lokale Suche feuert automatisch
- Treffer erscheinen direkt unter dem Suchfeld als Karten-Grid
- "Zuletzt hinzugefügt" wird ausgeblendet, sobald aktiv gesucht wird
- 0 Treffer + fertig gesucht → Inline-Button "Im Internet weitersuchen"

Header (nur auf /recipes/[id] und /preview):
- Gleiche Debounce-Logik, aber Treffer in einem Dropdown unterm Feld
- Dropdown: kompakte Zeilen mit Thumbnail, Titel, Domain
- Fußzeile des Dropdown: "Im Internet weitersuchen"
- Click-outside und Escape schließen das Dropdown
- afterNavigate setzt Query nach dem Klick auf einen Treffer zurück
- Header-Breite ist jetzt auf 760 px begrenzt (gleich wie Rezept-Content),
  damit die Suchleiste nie breiter wird als das Rezept darunter

Race-Safety: Ein zweites Tippen während laufender Fetch überschreibt
die Ergebnisse des ersten Requests nicht (Query-Vergleich vor Write).
2026-04-17 17:41:10 +02:00
hsiegeln
84655151be feat(nav): search lives in the header on all non-home pages
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
- "← Lokale Suche"-Breadcrumb auf der Web-Suchseite entfernt (überflüssig,
  da die lokale Suche automatisch zur Web-Suche weiterleitet, wenn leer).
- Header-Bar enthält jetzt ein Pill-Suchfeld, das von jeder Unterseite
  aus direkt auf /search?q=... navigiert — kein Zurück mehr nötig,
  wenn man aus einem offenen Rezept weiter sucht.
- Auf der Startseite bleibt die große Hero-Suche; das Header-Feld ist
  dort ausgeblendet, damit es keine doppelte Eingabestelle gibt.
- Auf /search und /search/web spiegelt das Header-Feld die aktuelle
  Query wider, sodass man den Begriff verfeinern kann.
- Mobile < 520px: Brand schrumpft zu einem 🍳-Badge, damit Platz für
  das Suchfeld + Icons bleibt.
2026-04-17 17:31:08 +02:00
4f7c76c908 feat(ui): custom dialog replaces all remaining window.alert() calls
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
alertAction({title, message}) returns Promise<void> and renders the
same ConfirmDialog with infoOnly:true — single OK button, no Abbrechen.
Replaces:
- 'Bitte Profil wählen.' (recipe rating / favorite / cooked / comment)
- 'Bitte Profil wählen, um zu liken.' (wishlist)
- 'Profil konnte nicht angelegt werden' (ProfileSwitcher)
- 'Umbenennen fehlgeschlagen' (admin/profiles)
- 'Speichern fehlgeschlagen' (preview)

No window.alert() or window.confirm() left in the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:23:07 +02:00
1b9928f806 feat(ui): custom confirmation dialog replacing native window.confirm
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 51s
Single reusable dialog with a promise-based API: confirmAction({...})
returns Promise<boolean>. Supports title, optional message body,
confirm/cancel labels, and a 'destructive' flag that paints the confirm
button red.

Accessibility: Escape cancels, Enter confirms, confirm button auto-focus,
role=dialog + aria-labelledby, backdrop click = cancel.

Rolled out to: recipe delete, domain remove, profile delete, wishlist
remove. Native confirm() is gone from the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:15:21 +02:00
3b1950713f feat(ui): wishlist page, recipe toggle button, header link
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 54s
- /wishlist renders cards with avatar-badge of who added it, like count,
  heart toggle for active profile, delete button. Sort dropdown switches
  between popular / newest / oldest.
- /recipes/[id] gets 'Auf Wunschliste (setzen)' button alongside favorite.
- Layout header shows 🍽️ link to /wishlist next to the admin ⚙️.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:08:22 +02:00
28e40d763d feat(api): wishlist endpoints (list, add, remove, like, unlike)
GET /api/wishlist?sort=popular|newest|oldest&profile_id=…
POST /api/wishlist { recipe_id, profile_id? }
DELETE /api/wishlist/[recipe_id]
PUT    /api/wishlist/[recipe_id]/like { profile_id }
DELETE /api/wishlist/[recipe_id]/like { profile_id }

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:08:22 +02:00
18547a7301 feat(wishlist): add shared family wishlist with likes
Each recipe appears at most once on the wishlist. Any profile can add,
remove, like, and unlike. Ratings and cooking log stay independent.

Data model: wishlist(recipe_id PK, added_by_profile_id, added_at)
            wishlist_like(recipe_id, profile_id, created_at)

Why: 'das will ich essen' — family members pick candidates, everyone
can +1 to signal agreement, cook decides based on popularity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:08:22 +02:00
72019f9cb7 fix(ui): show remote image URL in preview, local file after save
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 51s
In the preview flow, recipe.image_path is still the external recipe-page
URL (we only download the image when persisting). The RecipeView
component always prefixed with /images/ and produced a 404.

Now RecipeView detects http(s):// prefix and uses it directly, else
treats the value as a local filename under /images/.

Also adds referrerpolicy=no-referrer on the preview image so remote
CDNs don't block us based on Origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:03:13 +02:00
570a524d86 fix(search): unblock SearXNG 403 — config + headers
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
SearXNG returned 403 for every query, logging
'X-Forwarded-For nor X-Real-IP header is set!'. Two fixes, both needed:

1. searxng/settings.yml was being overwritten by SearXNG's default
   config in fresh volumes. Explicitly set limiter: false,
   public_instance: false, and move secret_key to env lookup via
   ${SEARXNG_SECRET:-…}. Force a well-known JSON format list.

2. Even with the limiter off, SearXNG's bot detection still nags on
   missing forwarder headers. The Node client now sends
   X-Forwarded-For: 127.0.0.1, X-Real-IP: 127.0.0.1 and Accept: json
   deterministically. Done via a new extraHeaders option on the http
   wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:56:13 +02:00
24058bcb77 fix(docker): healthcheck uses 127.0.0.1, not localhost
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 54s
In the alpine runtime 'localhost' resolves to ::1 (IPv6) first. The
SvelteKit Node adapter binds to 0.0.0.0 which is IPv4-only, so wget to
'localhost:3000' fails with 'Connection refused'. Traefik then filters
the container as unhealthy and no router is registered.

Using 127.0.0.1 makes the probe deterministically hit the bound IPv4
socket.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:47:48 +02:00
ec902049c1 fix(docker): tighter healthcheck cadence with start-period
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s
Traefik filters containers that are 'unhealthy' or still 'starting'
(no health result yet). The old 30s interval meant kochwas stayed in
'starting' for 30+ seconds after boot, blocking Traefik from routing to it.

New timing:
  --start-period=20s      grace window — failures don't mark unhealthy yet
  --start-interval=2s     fast probes during start-period
  --interval=15s          steady-state cadence after first success
  --timeout=5s, retries=3 unchanged

Container becomes 'healthy' within ~2-5s of the app coming up, so Traefik
picks it up almost immediately after 'docker compose up'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:45:00 +02:00
88125935d1 ci: switch build cache from type=gha to type=registry
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m19s
The Gitea Actions cache HTTP backend times out on export
(dial tcp 172.20.8.x:xxxxx: i/o timeout), failing the job even though
the image push itself succeeds.

Registry cache stores layer cache as a dedicated tag ':buildcache' in
the same container registry that already holds the images — reliable
and self-contained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:16:07 +02:00
f7fe216089 ci: use REGISTRY_TOKEN PAT for Gitea container registry login
Some checks failed
Build & Publish Docker Image / build-and-push (push) Failing after 24s
The auto-issued GITEA_TOKEN in Actions does not carry write:package scope,
so the docker login step failed with 'unauthorized'. Switching to a user-
supplied secret REGISTRY_TOKEN (PAT with write:package + read:package).

Setup on Gitea side:
1. Profile → Settings → Applications → Generate New Token
   with scopes write:package + read:package.
2. Repo → Settings → Actions → Secrets → add REGISTRY_TOKEN = <that PAT>.
   Optional: REGISTRY_USER if the owning account differs from gitea.actor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:10:00 +02:00
bf12468103 fix(infra): rename external network to traefik_proxy matching homelab setup
Some checks failed
Build & Publish Docker Image / build-and-push (push) Failing after 25s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:06:38 +02:00
32e7e54f4e feat(infra): add Traefik labels and network topology for homelab ingress
Some checks failed
Build & Publish Docker Image / build-and-push (push) Failing after 27s
- kochwas service gets Traefik v2 labels matching the project's conventions:
  websecure entrypoint, cloudflareResolver, Host(`kochwas.siegeln.net`).
- Service port 3000 exposed to Traefik only; the external port binding is gone.
- Dual network: external 'proxy' (for Traefik ingress) and internal 'internal'
  (for kochwas ↔ searxng). traefik.docker.network hint is set.
- SearXNG has no Traefik labels — intentionally only reachable from kochwas.

Note: the 'proxy' network name must match the existing external Traefik network
(change via 'name:' field if your homelab uses a different one like 'traefik').

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:05:06 +02:00
5fbb90745d ci: build for linux/arm64 (Raspberry Pi 5 target)
Some checks failed
Build & Publish Docker Image / build-and-push (push) Failing after 3m48s
Runner is arm64, so native build is much faster than amd64-via-QEMU.
Dev/test amd64 images can still be built locally with 'docker build'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:57:55 +02:00
3eb89cc5d6 ci: add Gitea Actions workflow to build and push Docker image
Some checks failed
Build & Publish Docker Image / build-and-push (push) Has been cancelled
On push to main (or version tag), the workflow logs into the Gitea container
registry, builds a multi-tag image (sha-<short>, branch name, 'latest' on main,
version on tag) and pushes to gitea.siegeln.net/<owner>/<repo>.

docker-compose.prod.yml now pulls from the registry by default, with
KOCHWAS_TAG env var to pin a specific build.

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