# Implementierungsplan Status: **final** (2026-09-13, abgestimmt) · Basis: [`anforderungen.md`](anforderungen.md), [`architektur.md`](architektur.md), ADR 001–007 ## Prinzipien - **Vertikal geschnitten:** Jedes Milestone ist ein vollständiger UI→Logic→Data-Slice und am Ende lauffähig/testbar (kein Layer-Reihenbau). - **Ein Milestone = ein Review-Pausenpunkt.** Jeder MS wird einzeln abgenommen, bevor der nächste startet. - **Kein CI** – PHPUnit läuft manuell (`composer test`). - **Auth:** Web-UI über **Keycloak-OIDC** (realm `family`, eigener Client `essens`), Auto-Provision des lokalen Users (wie `svc-webui`). HA bezieht die Tagesansicht über `GET /api/today` mit **statischem API-Token** (`X-Api-Token` aus `.env`, kein Keycloak-Refresh für HA). Damit wird ADR-003 in M0 entsprechend überarbeitet. - **Deploy** (M5) erst nach expliziter Freigabe. ## Milestones ### M0 – Skelett + Auth-Grundgerüst - Symfony 7.4 LTS, PHP 8.4, FrankenPHP: `composer.json`, `config/` (framework, doctrine/SQLite, twig, routing), `public/index.php` - Struktur laut `architektur.md` leergelegt (`src/UI`, `src/Logic`, `src/Data`) - Keycloak-OIDC: manueller `OidcClient` (kein `league/oauth2-client`, kein Security-Bundle; vgl. ADR-003), Login/Callback/Logout im UI-Layer, `sub`/`email`-Claim → lokale User-Zeile (Auto-Provision) - Docker: `Dockerfile` (php:8.4-frankenphp), `compose.yaml` (Volume `data/` für SQLite, `.env`), Network `infra-traefik_traefik-net` (external), Traefik-Labels → `essen.ljh31.de` + ACME + redirect-to-https - `phpunit.xml`, `tests/`, htmx + SortableJS in `public/js/` - ADR-003 überarbeitet (Keycloak statt „kein Login") - **DoD:** Container läuft, `GET /` → Keycloak-Login, danach Hello-World-Seite ### M1 – CRUD-Slice (Model: `Mahlzeit` – Namenskonszept: Domäne DE, Tech-EN, s. architektur.md §0.1) - **Schritt 1 (Logik+Daten):** `src/Logic/Essen` – `Mahlzeit`-Model (+`MahlzeitStatus`, `Wochentag`), `Mahlzeit{Create,Update}Request/Response`-DTOs, `MahlzeitFactory`, `Mahlzeit{Provider,Processor}Interface`, `MahlzeitManager`, `Mahlzeit{Create,Update,Delete}UseCase`, `MahlzeitListQuery`, `MahlzeitNotFoundException` (→404), Events; `src/Data/Essen` – `MahlzeitEntity` (Tabelle `essens_mahlzeiten`, wochentage als CSV), `MahlzeitMapper/Provider/Processor`, Migration. Tests: UseCase/Model-Unit (Pure PHP, Fakes) + Provider/Processor-Integration (file-basierte SQLite, `var/test.db`). **DoD 1:** `composer test` grün, CRUD vollständig über Logic-API testbar. - **Schritt 2 (UI):** `MahlzeitController` + `templates/mahlzeiten/` + Routen `/mahlzeiten[/{id}]` (POST/Redirect/Flash, Wochentage als Checkboxes) + Controller-Feature-Test (eingeloggte Session). - **DoD:** Gericht anlegen/bearbeiten/löschen im Browser (als eingeloggte/r Nutzer) ### M2 – Status & Wochentage - `CheckOffDish`/`UncheckDish` (Status `aktiv`/`gecheckt`), `ArchiveDish` - Wochentag-Tags (Mo–So) setzen/ändern, Liste nach Tag gruppier-/filterbar - **DoD:** Check-Off via htmx-Toggle, Tagesansicht, Archiv-Tab - *Konkrete Umsetzung (M1-Konvention, Domäne DE):* `MahlzeitCheckOffUseCase` (geplant→gegessen) / `MahlzeitUncheckUseCase` (gegessen→geplant), beide idempotent, `MahlzeitStatusChangedEvent`. „Archiv“ = Read-View auf `status = gegessen` (kein eigener Status/Use-Case). Check-Off als htmx-Partial (`POST /mahlzeiten/{id}/checkoff|uncheck`), Tagesansicht gruppiert nach Wochentag (Mo…So, „Ohne Tag“), Archiv-Tab `GET /mahlzeiten/archiv`. ### M3 – Reorder + Inspiration - `ReorderDishes` (SortableJS-`change` → Position-Ints, persistiert) - `SuggestDishes`-Query: Slow 0.5 / Rarität 0.3 / Hotness 0.2 (ADR-006), 5 Vorschläge + „Weitere 5", 1-Klick-Adopt → `CreateDish` mit Referenz - **DoD:** Reorder bleibt nach Reload bestehen; Inspiration klickbar adoptierend - *Konkrete Umsetzung (M1-Konvention, Domäne DE):* Schritt 1 (Logik): `MahlzeitReorderUseCase` (neue ID-Reihenfolge → `sortOrder` neu nummerieren, nur geänderte Positionen updaten, `MahlzeitReorderedEvent`), `MahlzeitInspirationQuery` (Kandidaten = Namen mit ≥ 1 `gegessen`-Eintrag, Repräsentant = neuester `gegessen`-Eintrag, Score nach ADR-006 mit Defaults 0.5/0.3/0.2, `ausschluss`-IDs für „Weitere 5", Sortierung absteigend nach Score, bei Gleichstand alphabetisch), `MahlzeitAdoptUseCase` (Quelle → neue `geplant`-Kopie mit `gegessenAm = null`, `MahlzeitAdoptedEvent`). Schritt 2 (UI): `GET /mahlzeiten` = flache, per SortableJS sortierbare Planungsliste (nur `geplant`, Drag-Handle, `POST /mahlzeiten/reorder` mit JSON/Form `ids` → 204); Tagesansicht ausgelagert nach `GET /mahlzeiten/tage`; Inspiration unter `GET /inspiration` (5 Vorschläge, „Weitere 5" via `?ausschluss=…`, 1-Klick-`POST /mahlzeiten/{id}/adopt` mit Kontext-Erhaltung). ### M4 – Härtung & API - `X-Api-Token`-Guard (optional, `.env`) für `GET /api/today` (HA-ready) - Fehlerseiten, Logging, Backup-Hinweis (SQLite-Datei `data/`) - **DoD:** `curl -H "X-Api-Token: …" /api/today` liefert Tagesgericht-JSON; DoD-Punkte aus `anforderungen.md` abgehakt ### M5 – Deploy (erst nach Freigabe) - Stack auf `docker1:/root/stacks/svc-essensplanung`, `git pull && docker compose up -d` - Traefik: `essen.ljh31.de`, Let's-Encrypt - Keycloak: Client `essens` in realm `family` anlegen (Redirect-URI `https://essen.ljh31.de`) - **DoD:** Gesamt-DoD aus `anforderungen.md` erfüllt, erreichbar ohne LAN-IP ## Test-Strategie (quer) - PHPUnit je Milestone: UseCase/Model-Unit (Pure PHP, Logic-Dependencies als Fakes) + Integration-/Feature-Tests (HTTP/DB gegen file-basierte Test-DB `var/test.db`) - Manuell: `docker compose run app composer test` bzw. `vendor/bin/phpunit` - Kein CI (bewusst, s. Prinzipien) ## Abnahme-Historie | MS | Datum | Status | |----|----------|------------| | M0 | 2026-09-13 | abgenommen | | M1 | 2026-09-19 | abgenommen | | M2 | 2026-09-19 | abgenommen | | M3 | 2026-09-19 | abgenommen | | M4 | | offen | | M5 | | offen |