Backend - Backup covers every Prisma model (bundle v3); a guard test fails for any new model that is neither backed up nor explicitly excluded. Unsealed bundles may only add rows to 27 legacy master/field tables and never overwrite; counters, ledgers, settings, roles etc. need a sealed file. Shared import limit BACKUP_MAX_IMPORT_BYTES (default 50 MB, max 256 MB) with warnings in log and UI; replaced document files are removed after commit. - Post-commit side effects (automation, calendar sync, webhooks) run via runAfterCommit and never turn a committed write into a 500, so retries with the same Idempotency-Key no longer create duplicates. - Calendar outbox is multi-replica safe: per-entry claims with stale takeover, revision-based settle, new Graph event only on 404/410, transactionId on create, 30 s Graph timeout, URL-encoded ids, GUID validation for groupId, "run now" in background and resets FAILED. - Automation tasks are deduplicated under a row lock on the rule. - Update DTOs follow one null rule: omitted = unchanged, null clears a nullable field, null on a required field is a 400. - Varroa mites/day is recomputed on correction; withdrawal status is checked against the harvest date (?at=), bounded colonyIds; inspection create is audited; updates re-enqueue calendar entries. - Dashboard, statistics and apiary/colony guards treat WEAK, CONSPICUOUS and QUEENLESS colonies as living. - Prisma errors and body-parser errors are logged without query values or request bodies. - Migration 20261012100000_calendar_sync_claims; schema index name aligned with the existing database object. Frontend - Sticky save bar sticks again (no scroll container ancestors, card uses overflow-clip), safe-area insets, scroll-padding, FAB hidden on forms, sticky desktop sidebar. - Inspections record yes / no / not checked instead of silent defaults. - All living colonies are selectable; shared RecordSelect, EnumSelect, ChoiceGroup and TriStateField replace per-page copies; selects show names instead of ids; labels point at real controls. - Emptied optional fields are sent as null (emptyToNull) so every field can be cleared; dates default to the local day and are sent via toApiDate. - Harvest withdrawal warning uses the harvest date; backup page warns when a backup outgrows the import limit and lists skipped tables. Build and docs - Docker builds fail unless lint, typecheck and unit tests pass. - ARCHITECTURE.md updated (assumptions 9-12, 19-21, section 8) and spec docs/superpowers/specs/2026-10-12-plattform-rueckgrat-und-fahrplan-design.md for the backbone (api/worker/job roles, domain event outbox) and the feature roadmap T3-T7. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
28 KiB
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. SYNCEDwird nur bedingt gesetzt, eine parallele Änderung geht also nicht verloren.- Ein neuer Termin entsteht nur bei 404/410.
- Einträge werden mit
- 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-paddingsind 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,nullauf einem Pflichtfeld ergibt 400. - Kalender-Termine werden bei Datumsänderung mitgezogen.
- Dashboard und Statistik zählen alle lebenden Völker.
- Projektweite
- Logs und Schnittstellen:
- Logs ohne Abfragewerte und ohne Request-Bodys.
- „Jetzt synchronisieren“ läuft im Hintergrund.
groupIdmuss eine GUID sein,colonyIdsist begrenzt.
- Frontend:
- Gemeinsame Bausteine
RecordSelect,EnumSelect,ChoiceGroup,TriStateFieldundemptyToNullstatt Kopien. - Auswahlfelder zeigen Namen statt IDs.
- Das Datum geht zeitzonensicher an die API (
toApiDate).
- Gemeinsame Bausteine
- Backup:
- 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_ROLEgilt in der Entwicklungall. 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:
AppModulebleibt das HTTP-Modul.WorkerModuleimportiert nur die Fachmodule, die Handler brauchen, und registriert keine Controller. Es startet mitNestFactory.createApplicationContextplus 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-evaluationzeitbasierte Ereignisse, Abschnitt 3.4 task-templatesAufgaben aus Vorlagen erzeugen, statt erst beim Lesen von GET /taskscleanupIdempotenz-Einträge und abgelaufene Tokens app-backupIn-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 alsinitContainer(prisma migrate deploy) inapiundworker; der Advisory-Lock von Prisma verhindert doppelte Läufe. Der Seed läuft nur imapi-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):
- Er übernimmt bis zu 50 Ereignisse per
UPDATE … WHERE id IN (SELECT … FOR UPDATE SKIP LOCKED) RETURNING *. - Er ruft je Ereignis die registrierten Handler auf (
@OnDomainEvent('InspectionRecorded')). - Wiederholungen laufen mit exponentiellem Abstand: 30 s, 2 min, 10 min, 1 h, 6 h.
- Nach 8 Versuchen wird das Ereignis
FAILED. Es erscheint unter „Verwaltung > System“ mit dem Knopf „Erneut versuchen“. - Ü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:
WithdrawalEndedTaskDueQueenAgedVarroaMeasurementDueInspectionOverdue
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
WebhookDeliverymit Wiederholung und Zustellprotokoll. Heute gehen Webhooks beim Rollout verloren. - Automations-Aufgaben: Sie werden über
TasksServiceangelegt. 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 rohemwhere, Postgres normalisiert das Prädikat, und das führt zu dauerhafter Abweichung zwischen Schema und DB. Bleibt so. - Audit in der Transaktion:
AuditLogService.recordlä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 aufevents.record(tx, …)wandert auch der Audit-Eintrag in dieselbe Transaktion;recordbekommt 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_KEYundBACKUP_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:- MFA- und Webhook-Secrets mit dem alten, abgeleiteten Schlüssel entschlüsseln.
- Mit den neuen Schlüsseln verschlüsseln.
- Webhook-Secrets aus der Zeit vor der Verschlüsselung, die noch im Klartext liegen, gleich mitverschlüsseln.
- Danach
JWT_ACCESS_SECRETundJWT_REFRESH_SECRETrotieren. 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: falsein allen Pods.readOnlyRootFilesystem: true, mitemptyDirfür/tmp.- Base-Image per Digest gepinnt, kein Tag
latestmehr.
5.4 Öffentliche Herkunftsseite
- Cache:
Cache-Control: public, max-age=300, s-maxage=3600fürGET /public/batches/:lot, dazu eine Cloudflare-Cache-Regel und eine Rate-Limit-Regel für diesen Pfad. - Keine genaue Adresse: Statt
Apiary.locationgibt die Seite ein neues FeldApiary.publicLocationLabelaus, 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/baseplusdeploy/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:
- Overlay auf den Ist-Stand bringen. Der Diff muss leer sein.
- NetworkPolicies schärfen.
- Worker-Deployment und CronJobs anlegen, im API-Pod
APICORE_ROLE=apisetzen. - Schlüssel trennen (Job
rekey). - 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:
api1–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.recordin der Transaktion, Rollen-Auswahl beim Start, Schlüsselbund undrekey. - 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
@Cronaußerhalb voncli.js-Jobs.
- Manifeste:
kubectl kustomize deploy/k8s/overlays/encryin der CI, dazukubeconform.
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-configund 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.
WithdrawalEndederzeugt 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.amountwird 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.shelfLifeMonthsundpackagingSize.
-
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
ACTIVEwaren 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.amountam 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.