diff --git a/docs/superpowers/plans/2026-04-19-post-review-roadmap.md b/docs/superpowers/plans/2026-04-19-post-review-roadmap.md new file mode 100644 index 0000000..31f5760 --- /dev/null +++ b/docs/superpowers/plans/2026-04-19-post-review-roadmap.md @@ -0,0 +1,217 @@ +# Post-Review Roadmap 2026-04-19 + +> **Quelle:** `docs/superpowers/review/OPEN-ISSUES-NEXT.md` (Items A–I) + UAT `kochwas-dev.siegeln.net` (Branch `review-fixes-2026-04-18`, 2026-04-19). +> **Branch-Status:** Merge-ready — 8 atomare Commits, 184/184 Tests grün, svelte-check 0 Errors, UAT durchgeklickt (Profil, Suche, Rezept-Actions, Wunschliste, Preview, Admin, API-Shapes). +> **Goal:** Die nach dem Review-Branch offenen 9 Items in priorisierte Phasen übersetzen, damit jede einzeln via `/gsd-plan-phase` → `/gsd-execute-phase` abgearbeitet werden kann. +> **Architecture:** Keine Groß-Refactor-Phase, sondern getaktete Einzel-Phasen mit klarem Gate. Reihenfolge folgt Risiko × Wert: erst kleine Wins, dann eine strukturelle Phase (A), dann opportunistische. +> **Tech-Stack:** SvelteKit, TypeScript-strict, Zod, Vitest, Playwright-UAT, better-sqlite3, Service-Worker. + +--- + +## Merge-Entscheidung + +**Jetzt mergen.** Der Branch-UAT auf `kochwas-dev` war clean (siehe Session-Log 2026-04-19). Findings aus dem UAT: + +- Kommentar-Delete hat keinen UI-Button (MINOR, kein Branch-Regress — Zustand schon vor Refactor so). +- `/preview` ohne `?url=` bleibt im Dauer-Lader (MINOR, harmlos — niemand ruft die Route blank auf). + +Beide werden als LOW-Items unten aufgenommen, sind aber **kein Merge-Blocker**. + +--- + +## Tier-Zuordnung + +| Tier | Items | Wann | Aufwand total | +|------|-------|------|---------------| +| 1 — Schneller Cleanup-Batch | F, G, H, I | Direkt nach Merge | ~2 h | +| 2 — Phase Search-State-Store | A | Nächster größerer Slot | halber Tag | +| 3 — Phase SearXNG-Recovery | C | Wenn Rate-Limit-Schmerz konkret auftaucht | 1–2 h | +| 4 — Opportunistisch | B, D, E, + Kommentar-Delete, Preview-Guard | Trigger-basiert | reaktiv | +| 5 — Geparkt | yauzl / Phase 5b | Nur bei explizitem Bedarf | nicht geplant | + +--- + +## Tier 1 — Cleanup-Batch (1 Phase, 4 Items) + +**Phasenname-Vorschlag:** `Phase Cleanup-Batch nach Review-Fixes` (via `/gsd-new-phase` oder `/gsd-add-phase`). + +Alle vier Items touchen wenige Zeilen, sind LOW/MEDIUM, und lassen sich in 1–2 Commits pro Item sauber atomar committen. **Gebündelt statt einzeln**, weil Kontext-Overhead pro Einzelphase größer wäre als der Fix. + +### Item I — RecipeEditor auf `$derived` umstellen + +**Files:** `src/lib/components/RecipeEditor.svelte:28,97–102,113,121`, `src/routes/recipes/[id]/+page.svelte:43` + +Pattern aktuell: `let foo = recipe.bar` → Svelte-5-Warning, Snapshot-only, bricht bei In-Place-Mutation des Rezepts. + +**Plan pro Warnung:** +- [ ] Warning-Site auslesen, beurteilen: soll `foo` Mutations am `recipe` tracken oder bewusst ein Snapshot bleiben? +- [ ] Track-Fall: `let foo = $derived(recipe.bar)`. +- [ ] Snapshot-Fall: Variable umbenennen (z. B. `initialFoo`) und als `$state` deklarieren mit Kommentar `// intentional snapshot`. +- [ ] `npm run check` — 0 Warnings erwartet. +- [ ] `npm test` — grün. +- [ ] Commit: `refactor(editor): RecipeEditor auf $derived umstellen`. + +**Gate:** svelte-check 0 Warnings, alle Editor-Flows (Titel, Zutaten, Schritte) per Hand getestet — In-Place-PATCH zeigt aktualisierten Wert. + +### Item H — RecipeEditor Bild-Upload/Delete auf `asyncFetch` + +**Files:** `src/lib/components/RecipeEditor.svelte:54,83` + +**Warum zusammen mit I:** Gleiche Datei, gleicher Touch. + +- [ ] Zeile 54 (Upload): `const res = await fetch(...); if (!res.ok) alertAction(...)` → `await asyncFetch(...)`. +- [ ] Zeile 83 (Delete): dito. +- [ ] Error-Messages beibehalten. +- [ ] Test manuell: Bild hochladen + löschen in einem Test-Rezept. +- [ ] Commit: `refactor(editor): Bild-Upload/Delete auf asyncFetch`. + +**Gate:** Bild-Upload + Delete-Flow grün in manuellem Smoke; `npm run check` clean. + +### Item F — Inline UI-Constants in `src/lib/theme.ts` + +**Files:** Neu `src/lib/theme.ts`, Modify `ConfirmDialog.svelte`, `ProfileSwitcher.svelte`, weitere Call-Sites via `grep`. + +- [ ] `grep -rn "z-index:\|border-radius: 999\|setTimeout.*[0-9]{3,4}" src/lib/components src/routes` — Call-Sites auflisten. +- [ ] `src/lib/theme.ts` anlegen mit: `MODAL_Z_INDEX = 1000`, `POPOVER_Z_INDEX = 900`, `PILL_RADIUS = '999px'` (nur Werte, die wirklich mehrfach vorkommen — YAGNI). +- [ ] Call-Sites durchgehen, Inline-Werte durch Import ersetzen. +- [ ] `npm run check` + `npm test`. +- [ ] Commit: `refactor(ui): shared theme constants fuer z-index/radius`. + +**Gate:** Keine visuellen Änderungen beim Durchklicken (Confirm-Dialog, Profile-Switcher, Toast, Menü). + +### Item G — `requireProfile()` mit optionaler Message + +**Files:** `src/lib/client/confirm.svelte.ts` (oder wo `requireProfile` liegt), `src/routes/wishlist/+page.svelte:38` + +**Option A — minimal invasiv:** `wishlist/+page.svelte` belassen, Custom-Message-Konstante in der Datei. Dann **nur dokumentieren** im Kommentar der `requireProfile`-Funktion, dass die Wunschliste bewusst eigenständig ist. + +**Option B — DRY:** `requireProfile(message?: string): Profile | null` mit Fallback auf Default. + +- [ ] **Entscheidung zuerst** — Option A sparsamer, Option B konsistent. Ich empfehle **A**, weil die Custom-Message in der Wunschliste wirklich Kontext ist („um mitzuwünschen"), nicht nur Deko. Aber: wenn B, dann sauber mit Unit-Test. +- [ ] Commit: `refactor(client): requireProfile Custom-Message entscheiden` (je nach Entscheidung). + +**Gate:** Wunschliste zeigt beim Klick ohne Profil die korrekte Message; keine anderen Sites verhalten sich anders. + +--- + +## Tier 2 — Phase Search-State-Store (Item A) + +**Empfohlener Einstieg:** `/gsd-discuss-phase Search-State-Store` (per OPEN-ISSUES Empfehlung), nicht direkt `/gsd-plan-phase`. + +**Warum eigene Phase:** Touch `+page.svelte` (808 L) + `+layout.svelte` (678 L), Reactive-Glue zwischen Header-Search-Dropdown und Home-Search muss 1:1 übernommen werden. **UAT-pflichtig**, weil es keine UI-Tests gibt. + +**Scope-Sketch (für die Discuss-Phase):** + +- Neu: `src/lib/client/search.svelte.ts` — reaktiver Store mit `query`, `hits`, `loading`, `error`, `hasMore`, `search(q)`, `loadMore()`, `clear()`. +- Debounce (aktuell in `+page.svelte`) in den Store migrieren. +- Web-Fallback-Logik (lokal leer → Web-Suche) beibehalten — Store muss beide Modi kennen (`mode: 'local' | 'web'`). +- `+layout.svelte` Header-Dropdown zuerst migrieren (kleineres Surface), dann `+page.svelte`. +- Duplizierten `$state`-Block entfernen. + +**Verifikation pro Wave:** +1. Nach Store-Anlegen: Vitest-Unit-Tests für Store (mocked fetch). +2. Nach Layout-Migration: Browser-UAT Header-Dropdown auf Rezept-Seite + Startseite. +3. Nach Page-Migration: Browser-UAT Live-Suche (lokaler Treffer, Web-Fallback, Empty-State), inkl. Deep-Link `?q=xyz`. +4. Playwright-Script wiederholen (existiert aus 2026-04-19 UAT). + +**Gate:** Alle 3 UAT-Pfade clean; `+page.svelte` unter 700 L; `+layout.svelte` unter 600 L; `npm test` + `npm run check` grün. + +**Aufwand:** halber Tag (4–6 h). + +--- + +## Tier 3 — Phase SearXNG-Rate-Limit-Recovery (Item C) + +**Trigger:** Wenn konkreter Schmerz (User merkt „Suche liefert komische alte Sachen" oder SearXNG logt 429/403 gehäuft). + +**Scope:** + +- `src/lib/server/search/searxng.ts`: `lastFailureAt: Map` pro Domain. +- Exponentieller Backoff: bei wiederholtem 429/403 → 1 min → 5 min → 30 min (Cap). +- Response-Shape erweitern: `isStale?: boolean` wenn aus Cache nach Fail. +- UI: `src/routes/+page.svelte` Such-Ergebnisheader zeigt „Ergebnisse evtl. veraltet" wenn `isStale`. + +**Tests (TDD, Vitest):** + +- Simulierter 429 → nächster Call innerhalb 1 min geht nicht raus, Response aus Cache mit `isStale: true`. +- Nach 1 min Wartezeit → Call geht wieder raus. +- Nach erfolgreichem Call → Backoff-Zähler resettet. + +**Gate:** Tests grün; manuell: Fake-429 injizieren (z. B. über ENV-Toggle im Dev), UI zeigt Hinweis. + +**Aufwand:** 1–2 h. + +--- + +## Tier 4 — Opportunistisch (Trigger-gesteuert) + +Alle Items hier werden **nicht proaktiv** geplant. Sie warten auf ihren Trigger. + +### Item B — RecipeEditor/RecipeView in Sub-Components + +**Trigger:** Zweite Person arbeitet am Projekt mit, ODER Editor-Bug-Hunt wird unübersichtlich. + +**Scope-Sketch:** `IngredientRow.svelte`, `StepList.svelte`, `TimeDisplay.svelte`, `ImageUploadBox.svelte`. + +**Vorbedingung:** Item I muss zuerst durch sein (die pre-existing Warnings würden sonst in die Sub-Components wandern). + +### Item D — SW Zombie-Cleanup unter Drosselung + +**Trigger:** Nächster Service-Worker-Touch (z. B. neue Cache-Strategy oder Chunks-Manifest-Änderung). + +**Scope:** Mit DevTools-Throttling-Profil „Slow 3G" durchgehen, prüfen ob der 1500ms-Timeout in `pwa.svelte.ts` False-Positives triggert. Falls ja: Timeout konfigurierbar oder Heuristik verfeinern. + +### Item E — JSON-LD Parser Locale-Edge-Cases + +**Trigger:** Echter Import-Bug aus dem Alltag. + +**Scope:** Gezielter Test für die Fail-URL + Fix. Kein Vorab-Sprint. + +### Kommentar-Delete-UI (UAT 2026-04-19) + +**Status:** Kommentar-DELETE-Endpoint existiert, aber keine UI-Exposition. + +**Vorschlag:** In `src/routes/recipes/[id]/+page.svelte` Kommentar-Liste pro Eintrag ein 🗑-Button für den Autor (`comment.profile_id === profileStore.active?.id`). Mit `confirmAction`-Dialog. + +**Trigger:** Erster Wunsch, einen Kommentar loszuwerden. + +**Aufwand:** ~30 min. + +### Preview-ohne-URL-Guard (UAT 2026-04-19) + +**Status:** `/preview` ohne `?url=` bleibt im Dauer-Lader. + +**Vorschlag:** `src/routes/preview/+page.svelte` Zeile 33ff.: wenn `u` leer, `errored = 'Kein URL-Parameter gesetzt'` oder Redirect auf `/`. **2-Zeilen-Fix.** + +**Trigger:** Bevor jemand die Route bookmarked. + +**Aufwand:** 5 min — könnte man auch sofort in Tier 1 reinnehmen, ist aber so trivial, dass es ohne Phase geht. + +--- + +## Tier 5 — Geparkt + +### Phase 5b — ZIP-Backup-Restore via `yauzl` + +**Status:** Dokumentiert in `ARCHITECTURE.md:33` und `session-handoff-2026-04-17.md`. Dependency bleibt installiert. + +**Kein Plan.** Wird erst aktiviert, wenn jemand wirklich ein Backup-ZIP zurückspielen will. Dann: `/gsd-plan-phase Phase-5b-ZIP-Restore`. + +--- + +## Empfohlene Ausführungs-Reihenfolge + +1. **Merge** `review-fixes-2026-04-18` → `main`. +2. **Neuen Branch** `cleanup-batch-post-review` → Tier 1 (Items I + H zusammen in einem Wave, dann F, dann G). +3. **Merge** → Tier 2 Discuss: `/gsd-discuss-phase Search-State-Store`. +4. Tier 2 execution. +5. Tier 3 erst wenn der Trigger da ist, sonst Tier 4 abwarten. + +--- + +## Commit-Stil für alle Phasen + +- Deutsch, kleinteilig, eine Idee pro Commit. +- Body erklärt das *Warum* (Reference auf Item-Nummer aus diesem Doc). +- Nach jedem Commit `npm test` + `npm run check` grün. +- Push direkt nach Commit (CI baut Branch-Tag, siehe `docker.yml`).