Merge remote-tracking branch 'origin/main' into feat/erp-inventar-export
# Conflicts: # ARCHITECTURE.md
This commit is contained in:
commit
9c332d4716
310 files changed
+12177
-3746
No files matched your search
@@ -0,0 +1,485 @@
|
||||
# Plattform-Rückgrat und Ausbau-Fahrplan — Design
|
||||
|
||||
Stand: 2026-10-12 · Betrifft: ApiCore (Backend, Frontend, `deploy/k8s`)
|
||||
|
||||
## 1. Ziel
|
||||
|
||||
ApiCore soll die Imkerei-Dokumentation sein, die einem Imker am meisten Arbeit abnimmt: Was das System schon weiß, fragt es nicht noch einmal ab. Was als Nächstes zu tun ist, schlägt es selbst vor. Am Bienenstand lässt es sich mit Handschuhen und ohne Netz bedienen.
|
||||
|
||||
Technisch heißt das:
|
||||
|
||||
- Die Teile, die unterschiedlich belastet werden, laufen und skalieren getrennt im RKE2-Cluster: Anfragen, Hintergrundarbeit, zeitgesteuerte Jobs.
|
||||
- Jede Hintergrundarbeit ist sicher, auch wenn mehrere Pods gleichzeitig laufen.
|
||||
- Fachliche Folgen (Logik-Ketten) hängen an einem dauerhaften Ereignis-Rückgrat statt an direkten Service-Aufrufen.
|
||||
- Sicherheit hat Vorrang vor Bequemlichkeit und vor CPU-Ersparnis. Wo beides kollidiert, steht der Konflikt in diesem Dokument.
|
||||
|
||||
Dieses Dokument legt das Rückgrat fest (Abschnitte 2–9) und den Fahrplan für die fachlichen Teilprojekte (Abschnitt 10). Jedes Teilprojekt aus Abschnitt 10 bekommt vor der Umsetzung eine eigene Spec.
|
||||
|
||||
Grundlage sind vier Analysen vom 2026-10-12: Microservice-Reife, Automationen, UX/Design und Fachlücken mit Vergleich zu anderen Apps. Ihre Kernbefunde stehen in Abschnitt 11.
|
||||
|
||||
### Entscheidungen
|
||||
|
||||
| Frage | Entscheidung |
|
||||
|---|---|
|
||||
| Wie wird geschnitten? | **Ein Image, drei Laufrollen** (`api`, `worker`, `job`), eine gemeinsame Datenbank. Kein Schnitt nach Fachdomänen. |
|
||||
| Warum nicht Domänen-Dienste mit eigener DB? | Die Kette Volk → Ernte → Schleuderung → Charge → Abfüllung läuft über Fremdschlüssel durch drei Fachbereiche (`HarvestColony→Colony`, `BottlingBatch→HoneyBatch`). Getrennte Datenbanken würden die Pflicht verletzen, diese Kette relational abfragbar zu halten (CLAUDE.md). Außerdem: Abfüllung als Saga, fünffache Grundlast, 30–45 PT Aufwand. |
|
||||
| Eigener Pod für die öffentliche Herkunftsseite? | Nein. Stattdessen Cache und Rate-Limit an der Cloudflare-Kante und eine Antwort, die nichts Sensibles enthält (Abschnitt 5.4). Ein eigener Pod bleibt Option, falls Last oder Bewertung es verlangen. |
|
||||
| Ereignis-Rückgrat | Transactional Outbox `DomainEvent` in Postgres, verteilt vom Worker. Kein Message-Broker, weil der zusätzliche Pods und Betrieb kostet. Ein Relay zu NATS bleibt später ohne Umbau der Erzeuger möglich. |
|
||||
| Zeitgesteuerte Arbeit | Kubernetes-CronJobs mit `concurrencyPolicy: Forbid` statt `@Cron` im Prozess. Kein Leader-Election-Code nötig, im Leerlauf keine Kosten. |
|
||||
| Rate-Limits | Zähler in Postgres (UNLOGGED-Tabelle) statt im Speicher, sodass sie über alle Pods gelten. Davor als äußere Schicht Cloudflare-Rate-Limit-Regeln. Kein Redis. |
|
||||
|
||||
### Bereits erledigt (Paket 0, 2026-10-12)
|
||||
|
||||
Paket 0 behebt Fehler, die unabhängig vom Rückgrat schaden:
|
||||
|
||||
- **Backup:** Das In-App-Backup enthält alle Modelle. Ein Wächter-Test erkennt künftig neue Modelle, die im Backup fehlen.
|
||||
- **Nebenwirkungen nach dem Commit** (Automationen, Kalender-Sync, Webhooks) werfen keinen Fehler mehr. Eine Wiederholung durch die Offline-Warteschlange erzeugt deshalb keine Dubletten mehr.
|
||||
- **Kalender-Outbox:**
|
||||
- Einträge werden mit `SKIP LOCKED` übernommen, sodass mehrere Pods dieselbe Zeile nicht gleichzeitig verarbeiten.
|
||||
- `SYNCED` wird nur bedingt gesetzt, eine parallele Änderung geht also nicht verloren.
|
||||
- Ein neuer Termin entsteht nur bei 404/410.
|
||||
- **Daten:**
|
||||
- Die Varroa-Rate wird bei jeder Korrektur neu berechnet.
|
||||
- Die Wartezeit wird gegen das Erntedatum geprüft (`?at=`).
|
||||
- Das Anlegen einer Durchsicht schreibt einen Audit-Eintrag.
|
||||
- **Oberfläche:**
|
||||
- Alle lebenden Völker sind auswählbar.
|
||||
- Ja/Nein-Befunde haben die Stufe „nicht geprüft“.
|
||||
- Datumsvorgaben sind lokal statt UTC.
|
||||
- Der Speichern-Balken klebt am unteren Rand, Safe-Area und `scroll-padding` sind berücksichtigt.
|
||||
- **Image-Build:** bricht ab, wenn Lint, Typecheck oder Unit-Tests rot sind.
|
||||
- **Nachträge aus den Reviews:**
|
||||
- **Backup:**
|
||||
- Unversiegelte Dateien ergänzen nur noch 27 Stamm- und Feldtabellen, sie überschreiben nichts.
|
||||
- Das Import-Limit ist auf 256 MB gedeckelt, mit Warnung in der UI.
|
||||
- **Bearbeiten:**
|
||||
- Projektweite `null`-Regel: ein Feld leeren geht, `null` auf einem Pflichtfeld ergibt 400.
|
||||
- Kalender-Termine werden bei Datumsänderung mitgezogen.
|
||||
- Dashboard und Statistik zählen alle lebenden Völker.
|
||||
- **Logs und Schnittstellen:**
|
||||
- Logs ohne Abfragewerte und ohne Request-Bodys.
|
||||
- „Jetzt synchronisieren“ läuft im Hintergrund.
|
||||
- `groupId` muss eine GUID sein, `colonyIds` ist begrenzt.
|
||||
- **Frontend:**
|
||||
- Gemeinsame Bausteine `RecordSelect`, `EnumSelect`, `ChoiceGroup`, `TriStateField` und `emptyToNull` statt Kopien.
|
||||
- Auswahlfelder zeigen Namen statt IDs.
|
||||
- Das Datum geht zeitzonensicher an die API (`toApiDate`).
|
||||
- **TRUST_PROXY:** Die Netzliste mit Pod-Netz und Cloudflare hat schon eine eigene Session umgesetzt (Commits `570f4f9`, `d7f0d2c`).
|
||||
|
||||
## 2. Laufrollen
|
||||
|
||||
Die Rolle wird über die Umgebungsvariable `APICORE_ROLE` gewählt. Alle Rollen stammen aus demselben Image und Build.
|
||||
|
||||
| Rolle | Start | Inhalt | HTTP | Skalierung |
|
||||
|---|---|---|---|---|
|
||||
| `api` | `node dist/main.js` | alle Controller, Guards, Interceptoren; keine Hintergrundschleifen, keine `@Cron` | Port 3001 über Service/Ingress | HPA 1–2 |
|
||||
| `worker` | `node dist/worker.js` | Event-Dispatcher, Kalender-Outbox, Webhook-Zustellung | nur `/health` auf Port 3002, ohne Service und Ingress | fest 1, mehr möglich (alle Schleifen `SKIP LOCKED`) |
|
||||
| `job` | `node dist/cli.js <name>` | einmalige Läufe aus CronJobs | keins | je Lauf ein Pod |
|
||||
| `all` | `node dist/main.js` | `api` und `worker` in einem Prozess | Port 3001 | nur lokale Entwicklung und Tests |
|
||||
|
||||
- **Standardwert:** Ohne `APICORE_ROLE` gilt in der Entwicklung `all`. In Produktion (`NODE_ENV=production`) ist die Variable Pflicht, sonst bricht der Start ab. Damit läuft in Produktion nie versehentlich ein zweiter Worker im API-Pod.
|
||||
- **Module:** `AppModule` bleibt das HTTP-Modul. `WorkerModule` importiert nur die Fachmodule, die Handler brauchen, und registriert keine Controller. Es startet mit `NestFactory.createApplicationContext` plus einem kleinen HTTP-Server für `/health`.
|
||||
- Der Health-Check prüft die DB-Verbindung und das Alter des letzten Dispatcher-Durchlaufs. Ist der älter als 2 Minuten, gilt der Worker als nicht gesund.
|
||||
- **Jobs** in `cli.js`:
|
||||
|
||||
| Job | Zweck |
|
||||
|---|---|
|
||||
| `daily-evaluation` | zeitbasierte Ereignisse, Abschnitt 3.4 |
|
||||
| `task-templates` | Aufgaben aus Vorlagen erzeugen, statt erst beim Lesen von `GET /tasks` |
|
||||
| `cleanup` | Idempotenz-Einträge und abgelaufene Tokens |
|
||||
| `app-backup` | In-App-Bundle |
|
||||
|
||||
Jeder Lauf schreibt Start, Ende, Ergebnis und Fehler in `JobRun`. Die Seite „Verwaltung > System“ zeigt diese Läufe.
|
||||
- **Migration:** Sie läuft nicht mehr in `CMD`. Sie läuft als `initContainer` (`prisma migrate deploy`) in `api` und `worker`; der Advisory-Lock von Prisma verhindert doppelte Läufe. Der Seed läuft nur im `api`-initContainer. Der Hauptprozess startet ohne Prisma-CLI, das spart RAM und Startzeit.
|
||||
|
||||
## 3. Ereignis-Rückgrat
|
||||
|
||||
### 3.1 Tabellen
|
||||
|
||||
**`DomainEvent`**: Outbox, append-only.
|
||||
|
||||
| Feld | Bedeutung |
|
||||
|---|---|
|
||||
| `id` | cuid |
|
||||
| `type` | z. B. `InspectionRecorded` (Katalog in 3.4) |
|
||||
| `aggregateType`, `aggregateId` | betroffenes Fachobjekt |
|
||||
| `payload` | JSON, nur IDs und die für Handler nötigen Werte, keine Geheimnisse |
|
||||
| `actorUserId` | auslösender Benutzer, `null` bei System oder Job |
|
||||
| `occurredAt` | Zeitpunkt |
|
||||
| `status` | `PENDING`, `PROCESSING`, `DONE`, `FAILED` |
|
||||
| `attempts`, `availableAt`, `claimedAt`, `lastError` | für Wiederholung und übernommene, aber liegengebliebene Einträge |
|
||||
|
||||
**`DomainEventDelivery`**: je Ereignis und Handler eine Zeile mit eindeutigem `(eventId, handler)`. Handler laufen damit genau einmal wirksam, auch wenn ein Ereignis erneut zugestellt wird.
|
||||
|
||||
### 3.2 Schreiben
|
||||
|
||||
Fachservices schreiben Ereignisse **in derselben Transaktion** wie die Fachdaten (Skizze; ob `AuditLogService.record` heute einen Transaktions-Client annimmt, klärt der Plan):
|
||||
|
||||
```ts
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const inspection = await tx.inspection.create({ data });
|
||||
await this.auditLog.record({ ... }, tx);
|
||||
await this.events.record(tx, {
|
||||
type: 'InspectionRecorded',
|
||||
aggregateType: 'Inspection',
|
||||
aggregateId: inspection.id,
|
||||
payload: { colonyId: inspection.colonyId },
|
||||
});
|
||||
return inspection;
|
||||
});
|
||||
```
|
||||
|
||||
Ohne Commit gibt es kein Ereignis, und ein Commit ohne Ereignis ist nicht möglich. Damit gibt es auch keine Nebenwirkung, die einen gespeicherten Datensatz nachträglich in einen 500er verwandelt.
|
||||
|
||||
### 3.3 Verteilen
|
||||
|
||||
Ablauf im Dispatcher (Worker):
|
||||
|
||||
1. Er übernimmt bis zu 50 Ereignisse per `UPDATE … WHERE id IN (SELECT … FOR UPDATE SKIP LOCKED) RETURNING *`.
|
||||
2. Er ruft je Ereignis die registrierten Handler auf (`@OnDomainEvent('InspectionRecorded')`).
|
||||
3. Wiederholungen laufen mit exponentiellem Abstand: 30 s, 2 min, 10 min, 1 h, 6 h.
|
||||
4. Nach 8 Versuchen wird das Ereignis `FAILED`. Es erscheint unter „Verwaltung > System“ mit dem Knopf „Erneut versuchen“.
|
||||
5. Übernommene, aber liegengebliebene Einträge (`claimedAt` älter als 5 min) gibt der Dispatcher wieder frei.
|
||||
|
||||
Takt: Der Dispatcher prüft im Leerlauf alle 5 s. Zusätzlich weckt `LISTEN/NOTIFY` (`pg_notify('domain_event')` nach dem Commit) ihn sofort. So bleibt die Grundlast im Leerlauf bei einer billigen Abfrage pro 5 s.
|
||||
|
||||
### 3.4 Ereigniskatalog (erste Stufe)
|
||||
|
||||
| Ereignis | Erzeuger | erste Handler |
|
||||
|---|---|---|
|
||||
| `InspectionRecorded` / `InspectionCorrected` | inspections | Automation (weisellos, Futter, Schwarmzeichen), Kalender, Webhook |
|
||||
| `VarroaMeasured` | varroa | Automation (Schwelle), Webhook |
|
||||
| `TreatmentRecorded` / `TreatmentCorrected` | treatments | Kalender, Folgekontrolle, Webhook |
|
||||
| `FeedingRecorded` | feedings | Kalender, Folgekontrolle, Webhook |
|
||||
| `HarvestRecorded` | harvests | Kalender, Webhook |
|
||||
| `ColonyStatusChanged` / `ColonyEnded` | colonies, winter-checks | Automation, Aufräumen (Abschnitt 10, T4) |
|
||||
| `BatchBottled` | bottling | Webhook, Bestand |
|
||||
| `StockChanged` | stock | Webhook `stock.changed`, Mindestbestand |
|
||||
| `TaskCreated` / `TaskCompleted` | tasks, Automation, Vorlagen | Kalender |
|
||||
|
||||
**Zeitbasierte Ereignisse** erzeugt der Job `daily-evaluation`:
|
||||
|
||||
- `WithdrawalEnded`
|
||||
- `TaskDue`
|
||||
- `QueenAged`
|
||||
- `VarroaMeasurementDue`
|
||||
- `InspectionOverdue`
|
||||
|
||||
Jedes davon hat einen Schlüssel `(type, aggregateId, Tag)` und entsteht höchstens einmal.
|
||||
|
||||
### 3.5 Umstellung
|
||||
|
||||
Die Erzeuger stellen schrittweise um, ein Modul je Commit. Die direkten Aufrufe `automation.evaluate*`, `calendarSync.enqueue` und `webhooks.dispatch` werden zu Handlern.
|
||||
|
||||
- **Kalender-Outbox:** Sie bleibt als eigene Tabelle, weil sie schon funktioniert und mehrfach-sicher ist. Sie wird nur noch aus Handlern befüllt.
|
||||
- **Webhooks:** Sie bekommen eine Zustelltabelle `WebhookDelivery` mit Wiederholung und Zustellprotokoll. Heute gehen Webhooks beim Rollout verloren.
|
||||
- **Automations-Aufgaben:** Sie werden über `TasksService` angelegt. Damit landen sie im Kalender und im Audit, was heute fehlt.
|
||||
- **Dubletten:** Paket 0 verhindert doppelte Automatik-Aufgaben mit einer Zeilensperre auf der Regel (`SELECT … FOR UPDATE`). Einen partiellen Unique-Index gibt es bewusst nicht: Prisma 7.9 kann ihn nur als Preview mit rohem `where`, Postgres normalisiert das Prädikat, und das führt zu dauerhafter Abweichung zwischen Schema und DB. Bleibt so.
|
||||
- **Audit in der Transaktion:** `AuditLogService.record` läuft heute in vielen Services nach dem Commit. Scheitert er, entsteht derselbe Fall wie in Paket 0 (500 nach Commit, Dublette bei Wiederholung). Mit der Umstellung auf `events.record(tx, …)` wandert auch der Audit-Eintrag in dieselbe Transaktion; `record` bekommt dafür einen optionalen Transaktions-Client.
|
||||
|
||||
## 4. Mehrfach-Sicherheit (Checkliste)
|
||||
|
||||
| Stelle | heute | Ziel |
|
||||
|---|---|---|
|
||||
| Kalender-Sync | Paket 0: `SKIP LOCKED` | im Worker, unverändert |
|
||||
| Backup-Zeitplan | `SchedulerRegistry` im API-Prozess, mtime-Guard mit TOCTOU | CronJob `app-backup`, Einstellungen aus der DB, GUI zeigt `JobRun` |
|
||||
| Idempotenz-Aufräumen | `@Cron` 4 Uhr in jedem Pod | CronJob `cleanup` |
|
||||
| Aufgabenvorlagen | beim Lesen von `GET /tasks` | CronJob `task-templates` (täglich 05:00) und zusätzlich beim Lesen, CAS bleibt |
|
||||
| Throttler | Speicher je Pod | `PrismaThrottlerStorage` (UNLOGGED-Tabelle, Upsert mit Ablauf), dazu Cloudflare-Regeln |
|
||||
| Nummernkreise | atomar | unverändert; Jahreswechsel bei `{YYYY}` setzt den Zähler zurück (eigener Fix) |
|
||||
| Benachrichtigungen | ein `read`-Flag für alle Nutzer | `NotificationRead` je Nutzer (eigener Fix, gehört zu T4) |
|
||||
|
||||
## 5. Sicherheit
|
||||
|
||||
### 5.1 Schlüssel trennen
|
||||
|
||||
Heute ist `JWT_ACCESS_SECRET` zugleich JWT-Schlüssel, AES-Schlüssel für MFA- und Webhook-Secrets und Rückfall-Schlüssel fürs Backup. Wird er rotiert, sind alle MFA-Registrierungen und Webhook-Secrets ungültig. Bis vor Kurzem lag er im Klartext in der ConfigMap.
|
||||
|
||||
Ziel:
|
||||
|
||||
- **Neue Variablen:** `MFA_ENCRYPTION_KEY`, `WEBHOOK_ENCRYPTION_KEY` und `BACKUP_ENCRYPTION_KEY` (gibt es schon, wird Pflicht).
|
||||
- **Chiffrat mit Schlüssel-ID** (`k1:…`). Ein Schlüsselbund aus aktuellem und vorigem Schlüssel erlaubt Rotation ohne Datenverlust.
|
||||
- **Einmaliger Umschlüssel-Job** `node dist/cli.js rekey`:
|
||||
1. MFA- und Webhook-Secrets mit dem alten, abgeleiteten Schlüssel entschlüsseln.
|
||||
2. Mit den neuen Schlüsseln verschlüsseln.
|
||||
3. Webhook-Secrets aus der Zeit vor der Verschlüsselung, die noch im Klartext liegen, gleich mitverschlüsseln.
|
||||
- **Danach** `JWT_ACCESS_SECRET` und `JWT_REFRESH_SECRET` rotieren. Folge: Alle Sitzungen enden einmal, das ist gewollt.
|
||||
- Ohne die neuen Variablen startet die Produktion nicht.
|
||||
|
||||
### 5.2 Netz
|
||||
|
||||
NetworkPolicies mit Default-Deny für Ingress und Egress im Namespace.
|
||||
|
||||
| Pod | Ingress | Egress |
|
||||
|---|---|---|
|
||||
| frontend | Traefik | Backend-Service, DNS |
|
||||
| api | Traefik, frontend | Postgres, DNS; 443 zu öffentlichen IPs (Have I Been Pwned, Open-Meteo, Entra-Login) |
|
||||
| worker | keiner (Probes kommen vom Kubelet) | Postgres, DNS, 443 zu öffentlichen IPs (Graph, Webhook-Ziele) |
|
||||
| jobs | keiner | Postgres, DNS, Backup-Ziel |
|
||||
| postgres | api, worker, jobs, pg_dump-Job | keiner |
|
||||
|
||||
**Konflikt Sicherheit gegen Aufwand:** Canal (RKE2-Standard) kennt keine FQDN-Regeln. Egress lässt sich deshalb nur auf „443 zu öffentlichen IPs ohne private Bereiche“ begrenzen, nicht auf einzelne Hosts. Der SSRF-Schutz für Webhook-Ziele bleibt zusätzlich in der App (`webhook-url-guard.ts`). Wichtig ist: Das Frontend kommt nicht mehr an Postgres, das geht heute über `allow-same-namespace`.
|
||||
|
||||
### 5.3 Pods
|
||||
|
||||
- `automountServiceAccountToken: false` in allen Pods.
|
||||
- `readOnlyRootFilesystem: true`, mit `emptyDir` für `/tmp`.
|
||||
- Base-Image per Digest gepinnt, kein Tag `latest` mehr.
|
||||
|
||||
### 5.4 Öffentliche Herkunftsseite
|
||||
|
||||
- **Cache:** `Cache-Control: public, max-age=300, s-maxage=3600` für `GET /public/batches/:lot`, dazu eine Cloudflare-Cache-Regel und eine Rate-Limit-Regel für diesen Pfad.
|
||||
- **Keine genaue Adresse:** Statt `Apiary.location` gibt die Seite ein neues Feld `Apiary.publicLocationLabel` aus, z. B. „Nordhessen“. Ist es leer, erscheint nur das Land. Der Grund ist Diebstahlschutz: Bienenvölker werden gestohlen.
|
||||
|
||||
### 5.5 Shop-Schnittstelle
|
||||
|
||||
Lesen (`shop-sync:read`) und Schreiben (`shop-sync:write`) bekommen getrennte API-Schlüssel. Die Verwaltungsoberfläche weist darauf hin, wenn ein Schlüssel beide Scopes hat.
|
||||
|
||||
## 6. Manifeste als Quelle der Wahrheit
|
||||
|
||||
Heute weicht `deploy/k8s` vom Cluster ab:
|
||||
|
||||
| Repo | Cluster |
|
||||
|---|---|
|
||||
| nginx-Ingress, `example.com`, 2 Replicas | Traefik, `cloudflare-only`, 1 Replica |
|
||||
| keine NetworkPolicies | 3 NetworkPolicies |
|
||||
| Postgres ohne Härtung | Postgres gehärtet |
|
||||
|
||||
Wer die Repo-Dateien anwendet, verliert also die Härtung.
|
||||
|
||||
Ziel:
|
||||
|
||||
- Struktur `deploy/k8s/base` plus `deploy/k8s/overlays/encry` (Kustomize).
|
||||
- Das Overlay enthält Hosts, Traefik-IngressRoute mit Middleware, Image-Digests, PVC-Klassen und NetworkPolicies.
|
||||
- Vor jedem Rollout zeigt `kubectl diff -k deploy/k8s/overlays/encry`, was sich ändert. Nach dem Rollout ist der Diff leer.
|
||||
- Secrets bleiben außerhalb des Repos, wie bisher als Vorlage `secret.example.yaml`.
|
||||
|
||||
Rollout-Reihenfolge, jeweils nur mit Freigabe des Inhabers:
|
||||
|
||||
1. Overlay auf den Ist-Stand bringen. Der Diff muss leer sein.
|
||||
2. NetworkPolicies schärfen.
|
||||
3. Worker-Deployment und CronJobs anlegen, im API-Pod `APICORE_ROLE=api` setzen.
|
||||
4. Schlüssel trennen (Job `rekey`).
|
||||
5. HPA einschalten.
|
||||
|
||||
## 7. Ressourcen
|
||||
|
||||
| Pod | CPU request | CPU limit | RAM request | RAM limit |
|
||||
|---|---|---|---|---|
|
||||
| api | 50m | 500m | 192Mi | 512Mi |
|
||||
| worker | 20m | 200m | 128Mi | 256Mi |
|
||||
| frontend | 30m | 300m | 128Mi | 256Mi |
|
||||
| CronJob-Pods | 50m | 500m | 192Mi | 512Mi |
|
||||
|
||||
- **Warum Requests:** Heute stehen die Requests auf 0. Damit sieht der Scheduler die Last nicht, und ein CPU-HPA kann nicht rechnen.
|
||||
- **Mehrkosten:** Im Leerlauf kommt etwa ein Worker-Pod hinzu, geschätzt 5–15m CPU und 150 MiB.
|
||||
- **Einsparung:** Die API startet ohne Prisma-CLI und hat keine Cron-Schleifen mehr. Das Backup läuft nicht mehr im API-RAM, der heute beim Export die ganze Datenbank plus alle Dokumente als Base64 hält.
|
||||
- **HPA:** `api` 1–2 Pods bei 70 % CPU.
|
||||
|
||||
## 8. Fehlerfälle
|
||||
|
||||
| Fall | Verhalten |
|
||||
|---|---|
|
||||
| Worker fällt aus | Ereignisse bleiben `PENDING`. API arbeitet weiter. Health-Probe startet den Worker neu. „Verwaltung > System“ zeigt das Alter des ältesten offenen Ereignisses. |
|
||||
| Handler wirft dauerhaft | Ereignis nach 8 Versuchen `FAILED`. Andere Handler desselben Ereignisses laufen trotzdem (eigene Delivery-Zeile). |
|
||||
| Graph oder Webhook-Ziel nicht erreichbar | Wiederholung mit Abstand. Kein Einfluss auf Anfragen. |
|
||||
| CronJob scheitert | `JobRun` mit Fehler. Benachrichtigung „Job X fehlgeschlagen“ an Nutzer mit `backup:manage` bzw. `automation:manage`. |
|
||||
| Zwei API-Pods | Throttler über die DB. Alle Hintergrundarbeit liegt im Worker oder in CronJobs. |
|
||||
| Migration scheitert | initContainer scheitert, der alte Pod läuft weiter (RollingUpdate). |
|
||||
|
||||
## 9. Tests
|
||||
|
||||
- **Unit:** Dispatcher (Übernahme, Wiederholung, Freigabe liegengebliebener Einträge, `FAILED`), `events.record` in der Transaktion, Rollen-Auswahl beim Start, Schlüsselbund und `rekey`.
|
||||
- **e2e gegen die Test-DB:**
|
||||
- Zwei Dispatcher-Instanzen parallel ergeben genau eine Wirkung je Handler.
|
||||
- Eine Durchsicht ergibt genau eine Automations-Aufgabe, auch wenn ein Handler einmal scheitert und wiederholt wird.
|
||||
- Throttler über zwei App-Instanzen.
|
||||
- **Wächter-Tests:**
|
||||
- Jedes Prisma-Modell steht im Backup oder in der begründeten Ausnahmeliste (gibt es seit Paket 0).
|
||||
- Kein `@Cron` außerhalb von `cli.js`-Jobs.
|
||||
- **Manifeste:** `kubectl kustomize deploy/k8s/overlays/encry` in der CI, dazu `kubeconform`.
|
||||
|
||||
Aufwand Rückgrat: Rollen und Jobs 2 PT, Ereignis-Rückgrat 3 PT, Throttler 0,5 PT, Schlüssel trennen 1,5 PT, Manifeste und NetworkPolicies 2 PT. Zusammen etwa 9 PT.
|
||||
|
||||
## 10. Fahrplan der fachlichen Teilprojekte
|
||||
|
||||
Jedes Teilprojekt bekommt vor dem Bau eine eigene Spec. Die Reihenfolge folgt dem Nutzen für den Imker im Verhältnis zum Aufwand. T3 kann parallel zum Rückgrat laufen. T4 braucht das Rückgrat.
|
||||
|
||||
### T3 Erfassung am Stand (etwa 6 PT)
|
||||
|
||||
- **Standrunde:**
|
||||
- Die Stand-Seite zeigt ihre Völker mit Ampel und dem Knopf „Runde starten“.
|
||||
- Nach dem Speichern kommt „Weiter mit B3“ statt eines Sprungs zur Volksseite.
|
||||
- Heute zeigt die Stand-Seite gar keine Völker.
|
||||
- **Durchsicht neu:**
|
||||
- Die Werte der letzten Durchsicht sind sichtbar vorbelegt.
|
||||
- Das Wetter kommt automatisch aus den Stand-Koordinaten, statt Freitext.
|
||||
- Waben werden über ein Plus/Minus-Feld gezählt.
|
||||
- Alle Skalen laufen in dieselbe Richtung (5 = gut).
|
||||
- Ein Foto lässt sich direkt mit der Kamera aufnehmen, auch offline.
|
||||
- **Varroa:**
|
||||
- Zeitraum in Tagen (1/2/3/7) statt Minuten.
|
||||
- Milben pro Tag erscheinen sofort, mit Ampel.
|
||||
- Der Trend zeigt Milben pro Tag mit Schwellenlinie.
|
||||
- **Sammelerfassung:**
|
||||
- Behandlung, Fütterung und Varroa für einen ganzen Stand oder eine Auswahl von Völkern.
|
||||
- Ergebnis ist ein Datensatz je Volk mit gemeinsamer `batchNumber`, alles in einer Transaktion.
|
||||
- Mittel, Wirkstoff, Wartezeit, Lieferant und Tierarzt werden aus der eigenen letzten Behandlung mit demselben Mittel vorbelegt.
|
||||
- **Offline-Standmodus:**
|
||||
- „Stand laden“ holt Völker, Königinnen, die letzten 3 Durchsichten und offene Aufgaben auf das Gerät.
|
||||
- Echter Kaltstart ohne Netz: App-Hülle, `runtime-config` und Sitzung offline.
|
||||
- Anfragen haben ein Timeout von 4 s.
|
||||
- Ein Band zeigt „Offline – X Einträge warten“.
|
||||
- **Navigation:**
|
||||
- Ein „Mehr“-Sheet auf dem Handy, denn heute sind 11 Bereiche auf dem Handy nicht erreichbar.
|
||||
- „+ Neu“ wird ein Sheet mit Icons und 56-px-Zeilen, auch auf dem Desktop.
|
||||
- Zurück-Link auf allen `new`-Seiten.
|
||||
|
||||
### T4 Logik-Ketten und Saison-Autopilot (etwa 10 PT, braucht das Rückgrat)
|
||||
|
||||
- **Durchsicht → Volkskarte:**
|
||||
- Die Werte der Durchsicht werden als Vorschlag zur Übernahme angeboten, einzeln abwählbar.
|
||||
- Geschrieben wird über `ColoniesService.update`, damit Audit und Regeln greifen.
|
||||
- Ein Vokabular für den Futterzustand statt String gegen Enum.
|
||||
- **Varroa-Ampel nach Jahreszeit (LWG):**
|
||||
|
||||
| Methode / Zeitraum | Grün | Gelb | Rot |
|
||||
|---|---|---|---|
|
||||
| Natürlicher Milbenfall pro Tag | unter 5 | 5–10 | über 10 |
|
||||
| Puderzucker, Juli | unter 1 % | 1–5 % | über 5 % |
|
||||
| Puderzucker, September | unter 2 % | 2–5 % | über 5 % |
|
||||
|
||||
- Ab November: Behandlung schon über 0,5 Milben/Tag.
|
||||
- Jungvölker: halbe Schwelle.
|
||||
- Werte konfigurierbar je Regel.
|
||||
- Nach einer Maßnahme: Nachkontrolle frühestens nach 14 Tagen.
|
||||
- **Fälligkeit der Durchsicht je Volk:** Intervall nach Saison, in der Schwarmzeit 7–9 Tage, sonst 14–21. Erscheint in der Heute-Ansicht. Das erfüllt das Erfolgskriterium §30 des Lastenhefts.
|
||||
- **Wartezeit-Kette:**
|
||||
- Das Wartefenster erscheint als Spanne im Kalender.
|
||||
- `WithdrawalEnded` erzeugt die Meldung „Volk erntefähig“.
|
||||
- Ernte, Schleuderung und Abfüllung prüfen serverseitig gegen das Erntedatum. Ergebnis ist eine Warnung plus Meldung, nie eine Sperre (Annahme 10).
|
||||
- **Ernte → Schleuderung → Charge vorbelegt:**
|
||||
- Schleudermenge = Summe Netto, dazu Sorte und Wassergehalt.
|
||||
- Plausibilitätswarnungen: Schleudermenge größer als Ernte, Summe der Chargen größer als Schleudermenge.
|
||||
- `HoneyBatch.amount` wird nur noch über das Bewegungsbuch geändert. Eine Korrektur ist weiter möglich, schreibt aber eine Korrekturbuchung.
|
||||
- **Qualität vor der Abfüllung:**
|
||||
- Wassergehalt über einer Grenze je Sorte (Standard 18 % D.I.B., 20 % HonigV): Warnung, über 20 % eine Bestätigung.
|
||||
- MHD und Glasgröße kommen aus `Product.shelfLifeMonths` und `packagingSize`.
|
||||
- **Biotechnik-Fristen:**
|
||||
- Ableger: nach 9 Tagen Nachschaffungszellen, nach 21–24 Tagen Begattung, nach 28 Tagen Stifte prüfen.
|
||||
- Königin zugesetzt: nach 7 Tagen Annahme prüfen.
|
||||
- Weiselzellen in der Durchsicht: Schwarmkontrolle in 7 Tagen.
|
||||
- **Volk beendet:**
|
||||
- Offene Aufgaben werden als erledigt bzw. obsolet angeboten.
|
||||
- Die Zuordnung der Königin wird beendet, auch beim Verlust über die Auswinterung.
|
||||
- **Saison-Autopilot:**
|
||||
- Je Volk „die nächsten 3 Schritte“ mit Begründung.
|
||||
- Grundlage: Volkszustand, Datum und Pflanzenentwicklung. Die kommt aus der Grünlandtemperatursumme über Open-Meteo oder aus einer eigenen Meldung per Antippen, z. B. „Salweide blüht“.
|
||||
- Kein ML, täglicher Job.
|
||||
- **Vorschlagsliste:** Automatische Aufgaben bleiben Vorschläge (Annahme 12), mit „Übernehmen“ und „Verwerfen“.
|
||||
|
||||
### T5 Optik mit eigener Handschrift (etwa 5 PT)
|
||||
|
||||
- **Waben-Standkarte:**
|
||||
- Jeder Stand ist eine Wabe, jedes Volk eine Zelle.
|
||||
- Die Füllung zeigt den Zustand über die `condition-*`-Tokens plus Icon, die Randstärke die Tage seit der letzten Durchsicht.
|
||||
- Auf der Karte ersetzen Mini-Waben die Standard-Marker von Leaflet.
|
||||
- **Lebenslinie des Volks:**
|
||||
- Senkrechte Zeitleiste mit Stärkekurve und einem Band für Milben pro Tag.
|
||||
- Die Königin erscheint als Punkt in ihrer echten Zeichenfarbe.
|
||||
- **Bienenjahr-Rad auf dem Dashboard:**
|
||||
- Ring mit Tracht, Schwarmzeit, Varroa-Fenstern und Einwinterung.
|
||||
- Ein Zeiger steht auf „heute“, offene Aufgaben sitzen als Punkte am Ring.
|
||||
- Ersetzt die Zählkacheln.
|
||||
- **Sonnentauglicher Kontrast:**
|
||||
- Text auf dem Hauptknopf mindestens 4,5:1, Rahmen mindestens 3:1, Grundschrift 16 px.
|
||||
- Optional ein Kontrastmodus.
|
||||
- Dark Mode wird angeschlossen, sonst fliegen die Tokens raus.
|
||||
- Manifest in Markenfarben mit PNG- und maskable-Icons.
|
||||
- **Baukasten durchsetzen:**
|
||||
- Je ein Baustein für Auswahlgruppe, Checkbox, Tabs und Plus/Minus-Feld.
|
||||
- Feste Icon- und Farbzuordnung pro Bereich.
|
||||
- Fokusstil für alle eigenen Bedienelemente.
|
||||
|
||||
### T6 Compliance-Autopilot „prüfungsfest“ (etwa 6 PT)
|
||||
|
||||
- **Tierarzneimittel-Katalog:**
|
||||
- Felder: Wirkstoff, Zulassung, Wartezeit, Dosis je Beute, Temperaturfenster, Lieferant.
|
||||
- Der Katalog wird vom Imker gepflegt, nicht hartkodiert (Annahme 10).
|
||||
- Er füllt die Behandlung vor und bucht den Lagerabgang. Das Bestandsbuch kommt dann ohne „FEHLT“ aus.
|
||||
- **Etikett-Pflichtangaben** nach HonigV und LMIV:
|
||||
- Ursprungsland, Los, MHD, Füllmenge, Name und Anschrift.
|
||||
- Seit 14.06.2026 bei Mischungen die Länderanteile in Prozent.
|
||||
- Fehlt eine Angabe, wird der Druck gesperrt und die Lücken werden aufgelistet.
|
||||
- **Behördenpaket:**
|
||||
- Ändern sich Völkerzahl oder Stand, kommt ein Hinweis plus PDF zur Anzeige beim Veterinäramt.
|
||||
- Zum Stichtag der Tierseuchenkasse wird die Zahl automatisch ermittelt.
|
||||
- Liegt ein Stand im Sperrbezirk, ist das Umsetzen gesperrt.
|
||||
- **Wanderung nach BienSeuchV §5:**
|
||||
- Bescheinigung als Dokument mit Ablaufprüfung (höchstens 9 Monate, nicht vor dem 1.9. des Vorjahres).
|
||||
- PDF für das Stand-Schild.
|
||||
- Rückkehr als Aufgabe.
|
||||
- **Seite „Prüfbereitschaft“:** Alles Offene erscheint rot.
|
||||
|
||||
### T7 Freihand und Sensoren (etwa 8 PT)
|
||||
|
||||
- **Spracherfassung:**
|
||||
- Stufe 1: Spracherkennung des Browsers.
|
||||
- Stufe 2: Ton wird offline aufgenommen und in die Warteschlange gestellt. Ein Job schreibt ihn mit faster-whisper ab (CPU), die Claude-API füllt daraus die Durchsichtsfelder als JSON.
|
||||
- Gespeichert wird nie ohne Bestätigung, die Audiodatei wird danach gelöscht.
|
||||
- **Stockwaage:**
|
||||
- Ein allgemeiner Endpunkt plus Anbindung an BEEP.
|
||||
- Gewichtssturz im Mai/Juni → Schwarmverdacht; Zunahme → Honigraum geben; Winterzehrung → Futter prüfen.
|
||||
- TASKS.md nennt Sprache, KI und Sensoren „nicht umsetzbar“. Diese Einordnung wird bei T7 korrigiert.
|
||||
|
||||
### Bewusst nicht im Fahrplan
|
||||
|
||||
- Domänen-Dienste mit eigenen Datenbanken (siehe Entscheidungen).
|
||||
- Foto-Analyse von Brutwaben: Sie bräuchte GPU und Training und wäre unzuverlässig.
|
||||
- Native App: Die PWA mit Offline-Standmodus deckt den Bedarf.
|
||||
|
||||
## 11. Befunde der Analyse vom 2026-10-12 (Kurzfassung)
|
||||
|
||||
### Paket 0 (erledigt)
|
||||
|
||||
- **Backup:** Das In-App-Backup ließ ProductionBatch, RawMaterialLot, ProductionInput, BatchMovement, ShopStockRequest, HoneyBatchContainerAllocation, AutomationSequence, CalendarSyncSettings und BackupSettings aus.
|
||||
- **Dubletten:** Ein Fehler nach dem Commit gab den Idempotency-Key frei.
|
||||
- **Kalender-Outbox:** übernahm Einträge ohne Lock, verlor parallele Änderungen und legte bei 5xx neue Termine an.
|
||||
- **Varroa:** Die Rate wurde bei Korrektur nicht neu berechnet.
|
||||
- **Wartezeit:** Sie wurde gegen „jetzt“ geprüft statt gegen das Erntedatum.
|
||||
- **Audit:** Das Anlegen einer Durchsicht schrieb keinen Eintrag.
|
||||
- **Völkerauswahl:** Nur Völker mit Status `ACTIVE` waren wählbar. Schwache, auffällige und weisellose Völker fehlten.
|
||||
- **Durchsicht:** speicherte stille Vorgaben als Befund (Stifte = ja, Futter ausreichend).
|
||||
- **Datum:** Vorgaben kamen aus UTC.
|
||||
- **Speichern-Balken:** klebte nicht unten.
|
||||
|
||||
### Offen
|
||||
|
||||
**Rückgrat (dieses Dokument):**
|
||||
- Kein Ereignis-Bus.
|
||||
- Throttler im Speicher.
|
||||
- Backup-Zeitplan und Cron im API-Prozess.
|
||||
- Webhooks ohne Wiederholung.
|
||||
- Ein Schlüssel für vier Zwecke.
|
||||
- NetworkPolicies nur für Ingress; das Frontend erreicht Postgres.
|
||||
- Manifeste weichen vom Cluster ab.
|
||||
- CPU-Requests stehen auf 0.
|
||||
- Herkunftsseite gibt den Stand-Ort aus.
|
||||
|
||||
**T3:**
|
||||
- Stand-Seite ohne Völker.
|
||||
- 11 Bereiche auf dem Handy nicht erreichbar.
|
||||
- Varroa-Zeitraum in Minuten.
|
||||
- Kein Kaltstart ohne Netz.
|
||||
|
||||
**T4:**
|
||||
- Automations-Aufgaben fehlen im Kalender.
|
||||
- Vorlagen-Aufgaben entstehen nur beim Lesen.
|
||||
- Ein `read`-Flag für alle Nutzer.
|
||||
- `HoneyBatch.amount` am Bewegungsbuch vorbei änderbar.
|
||||
- Kein Grenzwert für den Wassergehalt.
|
||||
|
||||
**T5:**
|
||||
- Kontrast: Weiß auf Honig hat 2,3:1, Eingaberahmen 1,56:1.
|
||||
- Dark Mode nicht erreichbar.
|
||||
- Auswahlgruppen 7× von Hand gebaut, Checkboxen in 4 Varianten, Tabs in 3 Varianten.
|
||||
Reference in new issue
Block a user