Doku: Anforderungen + ADRs (Symfony, htmx, SQLite, HA-Consumer)
This commit is contained in:
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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)
|
||||||
@@ -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 |
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user