From f1515d301c9943eddb43634c8b0657ab67647528 Mon Sep 17 00:00:00 2001 From: Jens Date: Sun, 13 Sep 2026 10:25:53 +0200 Subject: [PATCH] Doku: Anforderungen + ADRs (Symfony, htmx, SQLite, HA-Consumer) --- Doku/adr/001-backend-symfony.md | 19 ++++ Doku/adr/002-frontend-htmx.md | 22 +++++ Doku/adr/003-single-user-mit-token.md | 20 +++++ Doku/adr/004-ha-consumer.md | 23 +++++ Doku/adr/005-sqlite.md | 21 +++++ Doku/adr/006-inspiration-algorithmus.md | 31 +++++++ Doku/adr/README.md | 23 +++++ Doku/anforderungen.md | 111 ++++++++++++++++++++++++ 8 files changed, 270 insertions(+) create mode 100644 Doku/adr/001-backend-symfony.md create mode 100644 Doku/adr/002-frontend-htmx.md create mode 100644 Doku/adr/003-single-user-mit-token.md create mode 100644 Doku/adr/004-ha-consumer.md create mode 100644 Doku/adr/005-sqlite.md create mode 100644 Doku/adr/006-inspiration-algorithmus.md create mode 100644 Doku/adr/README.md create mode 100644 Doku/anforderungen.md diff --git a/Doku/adr/001-backend-symfony.md b/Doku/adr/001-backend-symfony.md new file mode 100644 index 0000000..759d87f --- /dev/null +++ b/Doku/adr/001-backend-symfony.md @@ -0,0 +1,19 @@ +# 001 – Backend auf Symfony 7 (PHP) + +**Status:** accepted + +**Kontext.** Das Werkzeug braucht eine kleine CRUD-API, die später auch von +Home-Assistant und (ggf.) von mobilen Clients konsumiert wird. Der +Projektverantwortliche kennt Symfony und möchte die Logik im Backend tragen +(serverseitig), nicht im Frontend. + +**Entscheidung.** Symfony 7 (PHP 8.3) als Backend-Framework. Eigene schlanke +Controller mit JSON-Antworten, **kein ApiPlatform** (zu viel Magie für dieses +Anwendungsfeld). Repository-Schicht via Doctrine. + +**Konsequenzen.** +- + Konvention/Ökosystem des Betreibers, kein zweites Ökosystem. +- + Doctrine + Symfony-Forms decken CRUD, Validierung und Security-Grundbaukästen ab. +- + Einfach zu hosten: FrankenPHP (oder php-fpm + nginx) in einem Container, keine zweite Laufzeit. +- − Keine SPA-Features, kein React-Native-Ökosystem – passt aber zur Anordnung. +- − Symfony hat eine Learning-Curve für neue Team-Mitglieder; im aktuellen Kontext kein Problem. diff --git a/Doku/adr/002-frontend-htmx.md b/Doku/adr/002-frontend-htmx.md new file mode 100644 index 0000000..4a68b07 --- /dev/null +++ b/Doku/adr/002-frontend-htmx.md @@ -0,0 +1,22 @@ +# 002 – Frontend: serverseitig + htmx (kein React) + +**Status:** accepted + +**Kontext.** Die App ist eine CRUD-Liste mit Formularen, Drag-&-Drop-Reihenfolge +und einem kurzen „Inspiration"-Panel. Kein Multiplayer, kein Offline-First, keine +komplexe Client-State. Der Betrieb soll so wenig bewegliche Teile wie möglich +haben (kein `npm install` im Runtime, keine Build-Pipeline). + +**Entscheidung.** Serverseitiges Rendern via Symfony (Twig) + **htmx** (~15 kB) +für fragmentbasierte Updates + **SortableJS** (~20 kB) für Drag-&-Drop. +Kein Node-Toolchain im Deployment, keine Client-State-Verwaltung. + +**Konsequenzen.** +- + Sehr kleine Client-Oberfläche, kein Build-Step, keine Versionsschlachten. +- + Business-Logik bleibt in Symfony/PHP – eine einzige Quelle der Wahrheit. +- + Deploy = ein PHP-Container + statische Dateien (htmx, SortableJS). +- + Fragmentbasierte Updates sind für Listen/CHECKBOXen fast unsichtbar. +- − Keine echte Offline-First / kein Optimistic-Update über mehrere Entities – + reicht hier (nur Drag-&-Drop-Reihenfolge + Checkbox; beide sind schnell genug). +- − Bei starkem Wachstum an interaktiver UI kann später ein SPA-Client nötig werden + (API ist JSON, bleibt kompatibel). diff --git a/Doku/adr/003-single-user-mit-token.md b/Doku/adr/003-single-user-mit-token.md new file mode 100644 index 0000000..2d0fdc0 --- /dev/null +++ b/Doku/adr/003-single-user-mit-token.md @@ -0,0 +1,20 @@ +# 003 – Single-User, keine Authentifizierung + +**Status:** accepted + +**Kontext.** Das Werkzeug wird im privaten Homelab von **einer** Person (bzw. +Familie via LAN) genutzt. Ein Login-Flow wird zur Barriere, nicht zum Feature. +Später soll Home-Assistant die API konsumieren, und dort ist es einfacher, +einen Shared-Token zu haben statt ein OIDC-Realm zu betreiben. + +**Entscheidung.** Im MVP **kein Login**. Die API ist hinter dem bestehenden +Traefik-Routing `essen.ljh31.de` (ACME/TLS) erreichbar; wer die Domain kennt, +kann aufrufen. Optional (Phase 2): **Shared-API-Token** per Header +(`X-Api-Token`) für externe Clients wie HA. + +**Konsequenzen.** +- + Einfachster möglicher Pfad, kein Auth-State, kein JWT. +- + HA-Integration ist trivial (einfacher Token statt OIDC). +- − Kein Benutzer-Abgrenzen, keine Rechte. Im aktuellen Kontext kein Risiko + (LAN + eigene Domain). +- − Wenn es irgendwann öffentliches Hosting wird, ADR-003 ersetzen. diff --git a/Doku/adr/004-ha-consumer.md b/Doku/adr/004-ha-consumer.md new file mode 100644 index 0000000..b1f53db --- /dev/null +++ b/Doku/adr/004-ha-consumer.md @@ -0,0 +1,23 @@ +# 004 – Home-Assistant als Consumer, nicht als Host + +**Status:** accepted + +**Kontext.** Frage der Diskussion: „Wäre Home-Assistant möglich?". HA ist eine +**View-/Automatisierungsplattform**, kein Logik-Backend und keine +PHP/MySQL-Sprach-Umgebung. Es kann unsere Anwendung nicht „ausführen", aber den +API-Status **anzeigen** und **benachrichtigen**. + +**Entscheidung.** Symfony-API bleibt die Source of Truth. Home-Assistant wird +**nur als Consumer** angebunden: +1. (Phase 2, optional) HA holt `GET /api/today` per `rest`-Integration → + Sensor „Heute auf dem Plan" (Zeichenkette). +2. (Phase 2, optional) HA-Notify-Benachrichtigung „Heute: Bolognese" per + Automation auf den Sensor-Change. +3. **Keine** Primary-UI in HA/Lovelace. + +**Konsequenzen.** +- + HA-Integration bleibt ein dünnes Add-on, kein Muss. +- + Kein zweites Ökosystem, das die Logik dupliziert. +- + HA-Sensor ist trivial zu erzeugen (ein GET + ein Template). +- − Keine „Interaktions-Card" in HA (z. B. kein Abhaken aus HA) – bewusst, + UI bleibt in der htmx-App. diff --git a/Doku/adr/005-sqlite.md b/Doku/adr/005-sqlite.md new file mode 100644 index 0000000..701465c --- /dev/null +++ b/Doku/adr/005-sqlite.md @@ -0,0 +1,21 @@ +# 005 – SQLite als Datenbank (vorerst) + +**Status:** accepted + +**Kontext.** Einzelne Instanz, ein Nutzer, kleine Datengröße im KB/MB-Bereich +(ein Gericht-Eintrag ~ 10 Zeilen + Historik). Backup-Pipeline (borgbackup) +deckt bereits das gesamte Homelab ab. Es gibt keinen Bedarf an HA (High +Availability), Multi-Node oder komplexen Transaktionen. + +**Entscheidung.** **SQLite** via Doctrine (Symfony unterstützt SQLite natively +seit 4.x mit guter Migrations-Schicht). Ein Dateivolumen (`app.db`) im Docker-Stack. + +**Konsequenzen.** +- + Extrem einfach: keine DB-Instanz, kein User/Passwort, kein Netzwerk. +- + Snapshot-Backup via `cp app.db` oder Borg direkt. +- + Kein `docker-compose`-DB-Service (einer weniger Container). +- + SQLite hat eine saubere WAL-Modus-API, die für die Zugriffsrate ausreicht. +- − Keine Remote-Replikation, kein Replikations-Setup (hier erwünscht). +- − Falls wir später mehrere Instanzen brauchen, wäre Postgres die Alternative – + aber das ist nicht aktuell (viele Homelab-Instanzen nutzen ja Postgres, + wenn sie es brauchen). diff --git a/Doku/adr/006-inspiration-algorithmus.md b/Doku/adr/006-inspiration-algorithmus.md new file mode 100644 index 0000000..d105cf7 --- /dev/null +++ b/Doku/adr/006-inspiration-algorithmus.md @@ -0,0 +1,31 @@ +# 006 – Inspiration-Algorithmus: gewichteter Mix (offene Gewichtung) + +**Status:** proposed (Gewichtung offen, Algorithmus fix) + +**Kontext.** „Inspiration" schlägt **jeweils 5** Gerichte aus dem Archiv vor, +neue 5 auf Button. Der Betreiber möchte einen **Mix aus drei Kriterien**: +lange nicht gekocht, seltene (wenige Auftritte) und beliebte (häufig gekocht). +Genauere Gewichtung und die Definition von „seltene" / „beliebte" werden bei +der Implementierung festgelegt. + +**Entscheidung (Prinzip).** Score-Funktion `score(g)` mit drei Termen: +1. **Staleness**: `now - max(gegessen_am)` (normiert an die älteste + `gegessen_am`) → gewichtet mit `w_slow` (>0, primär, z. B. 0.5). +2. **Rarity**: `1 / (auftritt_count + 1)` (auftritt = Anzahl aller Einträge + mit gleichem `name`) → gewichtet mit `w_rare` (sekundär, z. B. 0.3). +3. **Popularity**: `auftritt_count / max(auftritt_count)` → gewichtet mit + `w_hot` (tertiär, z. B. 0.2). + +`score = w_slow*stale + w_rare*rare + w_hot*hot`, Sortierung absteigend. +`w_slow + w_rare + w_hot = 1.0`; konkrete Werte in `config/inspirations.yaml` +und per ADR-006-Bis (Patch) fixieren, wenn die App läuft. + +**Konsequenzen.** +- + Alle drei Nutzerintentionen (Alte, Seltene, Beliebte) sind vertreten. +- + Gewichte prozentual änderbar, ohne Code. +- − „Beliebte" ist kontraintuitiv, wenn der Nutzer genau das **nicht** + möchte (z. B. „mööchte etwas Neues"). Dann `w_hot=0`. +- Offene Fragen: + - [ ] Gewichte `w_slow/w_rare/w_hot` (Default: 0.5/0.3/0.2?) + - [ ] „Auftritt" zählt **Alle Zeit** oder **letzte N Tage**? (Vorschlag: alle Zeit) + - [ ] Deduping gegen die letzte 5er-Ladung? (Vorschlag: ja, Session-basiert) diff --git a/Doku/adr/README.md b/Doku/adr/README.md new file mode 100644 index 0000000..ef314ec --- /dev/null +++ b/Doku/adr/README.md @@ -0,0 +1,23 @@ +# Architecture Decision Records (ADRs) + +Kurze, nicht-änderbare Aufzeichnungen der wichtigsten Entscheidungen. +Format: ADR-Mikrometer-Muster (Kontext, Entscheidung, Konsequenzen). +Nummerierung: `NNN-Name-slug.md`, fortlaufend, Lücken zulässig. +Wenn sich etwas ändert: **neue** ADR, alte als `superseded` markieren. + +## Status +- `accepted` – gilt aktuell +- `proposed` – noch nicht endgültig +- `superseded` – durch neuere ADR ersetzt (→ verlinken) +- `rejected` – bewusst verworfen + +## Index + +| Nr | Titel | Status | +|----|-------|--------| +| [001](001-backend-symfony.md) | Backend auf Symfony 7 (PHP) | accepted | +| [002](002-frontend-htmx.md) | Frontend: serverseitig + htmx (kein React) | accepted | +| [003](003-single-user-mit-token.md) | Single-User, keine Authentifizierung (+ optionales API-Token) | accepted | +| [004](004-ha-consumer.md) | Home-Assistant als Consumer, nicht als Host | accepted | +| [005](005-sqlite.md) | SQLite als Datenbank (vorerst) | accepted | +| [006](006-inspiration-algorithmus.md) | Inspiration-Algorithmus: gewichteter Mix | proposed | diff --git a/Doku/anforderungen.md b/Doku/anforderungen.md new file mode 100644 index 0000000..44d8608 --- /dev/null +++ b/Doku/anforderungen.md @@ -0,0 +1,111 @@ +# Anforderungen – svc-essensplanung + +> Stand: 13.09.2026 · Status: **Agreed Draft** (noch nicht in Code umgesetzt) + +Kurzes Mittagessen-Planer-Werkzeug: Gericht anlegen, „fertig/gegessen" abhaken, +Reihenfolge anpassen, Wochentage markieren und bei Bedarf aus dem Archiv +inspirieren lassen. Ein einzelner Nutzer, läuft im eigenen Homelab, mobil via Browser. + +## 1. Kern-Entities + +### Gericht (Eintrag) +Ein Gericht ist die zentrale Dateneinheit. Felder: + +| Feld | Typ | Pflicht | Beschreibung | +|------|-----|---------|--------------| +| `id` | int (PK) | ja | Autogeneriert | +| `name` | text | ja | z. B. „Spaghetti Bolognese" | +| `notiz` | text | nein | Freie Anmerkung, z. B. „mit mehr Soße" | +| `menge` | int/smallint | nein | Portionen oder Stückzahl, falls nützlich | +| `wochentage` | set (Mo–So) | nein | Beliebig viele, optional. **Nur zur Markierung/Gruppierung**, keine Automatik. | +| `sort_order` | int | ja | Position in der Planungsliste (manuell per Drag & Drop) | +| `status` | enum: `geplant` / `gegessen` | ja | `geplant` = aktiv in der Liste, `gegessen` = abgehakt → Archiv | +| `gegessen_am` | date | nein | Setzt sich automatisch, wenn auf `gegessen` gesetzt | +| `erstellt_am` | timestamp | ja | | +| `aktualisiert_am` | timestamp | ja | | + +> **Wiederholungen über mehrere Tage** (z. B. Reste Mo+Di) werden **nicht** als +> automische Dauer modelliert. Stattdessen decken die `wochentage`-Markierungen und +> ggf. mehrere Einträge ab. „Erforderlich, dass jeder Tag abgedeckt ist" gilt **nicht** – +> Tage dürfen frei stehen bleiben. + +### Archiv +- Das Archiv ist **keine eigene Tabelle**: Es ist die Sicht auf Einträge mit + `status = gegessen`, geordnet nach `gegessen_am` (neueste zuerst). +- Abhaken (`geplant → gegessen`) verschiebt den Eintrag automatisch in die Archivsicht. +- Aufheben / „wieder einplanen" (`gegessen → geplant`) setzt `gegessen_am` zurück und + fügt ihn der Planungsliste wieder hinzu. + +## 2. Features (MVP) + +### 2.1 Gericht verwalten +- **Anlegen** (name, notiz, menge, wochentage). +- **Bearbeiten** (alle Felder). +- **Löschen** (hart, mit Bestätigung). +- **Reihenfolge ändern**: Drag & Drop in der Planungsliste → persistiert in `sort_order`. + +### 2.2 Abhaken / Archiv +- Pro Eintrag ein „Abhaken"-Aktion (Checkbox oder Button). +- Abhaken: `status → gegessen`, `gegessen_am = heute`, Eintrag verschwindet aus der + Planungsliste in die **Archivansicht**. +- In der Archivansicht: Eintrag wieder in die Planung zurückstellen oder Löschen. + +### 2.3 Inspiration +- **Inspiration-Panel**, das **jeweils 5 Vorschläge** aus dem Archiv anzeigt. +- Button „Weitere 5" → nächste 5, die noch nicht vorgeschlagen wurden. +- **Auswahlkriterium (gewichteter Mix):** + 1. **Lange nicht** gekocht (älteste `gegessen_am`) – primär, + 2. **Seltene** (wenigste Auftritte insgesamt) – sekundär, + 3. **Häufige** (beliebt) – drittens. + Genauere Gewichtung bei der Implementierung festlegen (siehe ADR-005, offen). +- Jeder Vorschlag hat einen **1-Klick-Button** → übernimmt das Gericht als **neuen + `geplant`-Eintrag** in die Planungsliste (Kopie, `gegessen_am` wird leer). + +### 2.4 Ansichten +- **Planung** (Liste der `geplant`-Einträge, sortiert nach `sort_order`). +- **Archiv** (Liste der `gegessen`-Einträge, sortiert nach `gegessen_am` absteigend). +- **Inspiration** (Panel / eigener Bereich, siehe 2.3). +- Wochentage werden je Eintrag **als Markierung** angezeigt (kein Filter nötig, aber + optional als Gruppierung möglich). + +## 3. Technische Vorgaben (aus der Diskussion) + +- **Backend:** PHP / **Symfony 7**, eigene JSON-Controller (keine ApiPlatform-Magie), + als **API** nutzbar. +- **Frontend:** Serverseitiges Rendering + **htmx** (CRUD + Listen + Formulare). + Sortierbarkeit via **SortableJS** (~20 kB). **Kein Build-Step, kein Node im Runtime.** +- **Datenbank:** **SQLite** (einfach, leicht zu backupn). +- **Mobile:** Responsive + **PWA-Manifest** („Zum Homescreen hinzufügen"). +- **Deployment:** Docker-Stack `svc-essensplanung` im Homelab, hängend an das + bestehende Traefik-`infra-traefik_traefik-net`; **Domain:** `essen.ljh31.de` + (ACME `letsencrypt`). Gleiche Deploy-Muster wie die anderen `svc-*`-Stacks. +- **Auth:** Single User → **kein Login**. Optionaler Shared-Token, falls später von + elsewhere (z. B. Home-Assistant) genutzt. + +### 3.1 Home-Assistant-Integration (Phase 2, optional) +- HA holt **„Was steht heute an?"** über dieselbe API. +- Ziel: 1 Zeile im HA-Dashboard + ggf. Benachrichtigung „Heute: Bolognese". +- Ist ein z**Add-on**, kein Teil des MVP. Siehe ADR-004 (offen, Phase 2). + +## 4. Ausdrücklich NICHT im MVP +- Mehrere Nutzer / Rechte. +- Automatische Wiederholung pro Wochentag (Wochentag ist nur Label). +- Einkaufslisten, Kalorien, Portionen-Logik. +- Offline-First / lokale Sync (htmx braucht die API). +- Export/import (vorerst). + +## 5. Offene Punkte (bevor Code) +- [ ] Genaue **Gewichtung** der Inspiration (s. ADR-005). +- [ ] `menge`: reine Zahl oder Zahl + Einheit (Portionen)? +- [ ] Soll **Löschen** hart sein oder auch auf archiviert? (vorerst: hart) +- [ ] PWA: nur Add-to-Home, oder auch Push (braucht SW + Service-Worker)? +- [ ] HA-Integration: konkret als Sensor (REST-Poll) via `rest`-Integration in HA — + dazu braucht die API einen sauberen `GET /api/today`-Endpunkt. + +## 6. Abnahmekriterien (Definition of Done, MVP) +- [ ] Gericht anlegen/bearbeiten/löschen läuft via UI. +- [ ] Reihenfolge per Drag & Drop persisted (übersteht Reload). +- [ ] Abhaken verschiebt Eintrag in Archiv, Wiederherstellen bringt ihn zurück. +- [ ] Inspiration zeigt 5, „Weitere 5" liefert neue 5, 1-Klick übernimmt. +- [ ] Läuft auf `essen.ljh31.de` (https, ACME) und ist mobil + add-to-home. +- [ ] Daten in SQLite, kein Build-Step im Deployment.