Files
ApiCore/docs/superpowers/specs/2026-10-12-plattform-rueckgrat-und-fahrplan-design.md
T
Nick SzaunigandClaude Opus 5.5 ec8c95056d
Backend Image / build-and-push (push) Successful in 6m43s
Frontend Image / build-and-push (push) Failing after 12m42s
Manifests / validate (push) Successful in 18s
docs: README im Root, Backend/Frontend-READMEs, Statuszeilen (2026-10-16)
Neues README im Root mit Zweck, Aufbau, Start, Prüfbefehlen, Betrieb und
Projektstand (Issues #44, #43). Backend- und Frontend-README ersetzen die
Framework-Vorlagen. CLAUDE.md: Ordnerstruktur und echte Arbeitsweise
(main, gebündelt pushen, Rollout nur mit Freigabe). ARCHITECTURE §7/§8:
Live-ConfigMap, ensure-secrets.sh, korrektes Admin-Seed-Verhalten.
.env.example um BACKUP_MAX_IMPORT_BYTES und SEED_ADMIN_* ergänzt.
Statuszeilen in Plänen Rückgrat, T3–T6 und Specs Fahrplan, T7.

Refs #42

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 08:12:49 +02:00

29 KiB
Raw Blame History

Plattform-Rückgrat und Ausbau-Fahrplan — Design

Status (2026-10-16): Rückgrat gebaut (#13), Paket 0 erledigt (#12), T3 (#14), T4 (#15) und T6 (#16) gebaut; nichts davon ausgerollt (#28). T5 (#26) und T7 (#27) haben eigene Specs, sind aber noch nicht gebaut. Die Liste „Offen“ in Abschnitt 11 ist der Befund vom 2026-10-12, nicht der heutige Stand.

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):

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.