Files
ApiCore/docs/superpowers/specs/2026-10-13-t4-logik-ketten-design.md
T
Nick SzaunigandClaude Opus 5.5 cdd30cf3ee feat: platform backbone and field capture complete (checkpoint)
Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed:
- webhooks with delivery table, retries and stable delivery id
- calendar handlers; a finished task removes its Outlook event
- tasks, automations, inspections, varroa, treatments, feedings,
  harvests, colonies, wintering, stock and bottling record domain events
  in the same transaction; legacy post-commit calls removed
- row locks through one helper (lockRowForUpdate, lockRowsForShare,
  lockRowsForUpdate)
- job framework with JobRun, cli exit codes, cleanup, task templates,
  daily evaluation, hourly app-backup and the rekey job
- system API and page "Verwaltung > System" with retry
- @nestjs/schedule removed, guard tests against in-process schedulers

Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed:
- inspection photo offline without metadata
- bulk capture for treatments, feedings and varroa (backend + frontend)
- offline stand mode with service worker, offline pages and packs
- offline session, cold start and logout on shared devices

Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md.
Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema
drift; frontend lint, typecheck, 913 tests, build. Not rolled out.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-14 11:36:38 +02:00

114 KiB
Raw Blame History

T4 Logik-Ketten und Saison-Autopilot — Design

Stand: 2026-10-13 · Betrifft: ApiCore (Backend, Frontend) · Teilprojekt T4 aus docs/superpowers/specs/2026-10-12-plattform-rueckgrat-und-fahrplan-design.md §10

1. Ziel

Was der Imker erfasst, wirkt weiter. ApiCore zieht daraus die Folgen selbst:

  • Aus einer Durchsicht, einer Messung oder einem Ableger entstehen datierte Folgeschritte.
  • Jedes Volk hat „die nächsten 3 Schritte“, jeweils mit einer kurzen Begründung.
  • Die Saison kommt aus der Pflanzenentwicklung am Stand, nicht aus einem starren Kalender.
  • Automatik plant nie heimlich. Sie schlägt vor. Der Imker übernimmt oder verwirft mit einem Tipp.

Technisch heißt das:

  • Alle Folgen hängen an Handlern des Ereignis-Rückgrats (@OnDomainEvent, events.record(tx, …)) oder am Job daily-evaluation. Kein T4-Code ruft Folgen direkt nach dem Commit auf.
  • Ein Regelwerk im Code, ohne ML. Jeder fachliche Wert ist je Regel einstellbar und nennt seine Quelle.
  • Ein Muster für alle Vorschläge: ein Modell, ein Service, eine Liste.

Voraussetzungen:

  • Rückgrat-Plan docs/superpowers/plans/2026-10-13-plattform-rueckgrat.md: Tasks 3, 4, 9, 10, 11–15, 17 und 18 sind fertig. T4 baut genau auf TasksService.createInTx, AutomationService.…In(tx, …), JobRunService, dailyDedupeKey und dem Handler-Vertrag auf.
  • T3-Plan docs/superpowers/plans/2026-10-13-t3-erfassung-am-stand.md: Tasks 2, 3, 9, 11 und 14 sind fertig. T4 ändert deren Ampeln und ergänzt das Durchsichtsformular. T4 baut nichts davon neu.

Inhaber-Entscheidungen (2026-10-13), bindend

  • Nur eigener deutscher Honig. Keine Wanderung. Keine Stockwaage. Alles andere aus §10 T4 ist gewollt.
  • Automatische Aufgaben bleiben Vorschläge mit „Übernehmen“ und „Verwerfen“ (Annahme 12).
  • Wartezeit-Prüfungen warnen, sperren nie (Annahme 10).
  • Beobachtungen werden nie still vorbelegt (T3-Regel).
  • Sicherheit hat Priorität 1. Ein Muster, gemeinsame Helfer. Jedes Feld bleibt bearbeitbar (Null-Regel, Annahme 20).
  • Deutsche Oberfläche, kurze Sätze, ADHS-freundlich: ein Hauptknopf je Ansicht, wenig kognitive Last. WCAG 2.2 AA.

Antworten des Inhabers auf die Spec-Fragen (2026-10-13), bindend

  1. Volksstärke an der Volkskarte = besetzte Waben, 0–40, dieselbe Einheit wie in der Durchsicht. Alte Werte (Skala 1–10) werden nicht umgerechnet. Sie bleiben mit ihrer Einheit lesbar (4.14).
  2. Wetter für die GTS: freie Open-Meteo-API. Der Inhaber weiß, dass sie laut Anbieter nur für nicht-kommerzielle Nutzung frei ist, und trägt dieses Risiko. Die Quelle steckt hinter einem kleinen Adapter, damit ein Wechsel später eine Einstellung ist (6.3).
  3. Erinnerungen: beides, eine Meldung je fälliger Aufgabe und eine Morgenübersicht je Nutzer. Zugestellt in der App und über einen Telegram-Bot als neuen Kanal (Abschnitt 9a).

Entscheidungen

Frage Entscheidung
Wie werden Vorschläge gespeichert? Eigenes Modell Suggestion, kein neuer TaskStatus. Eine Aufgabe bleibt „was ich mir vorgenommen habe“. Kalender, Webhooks, Export, Suche und Offline-Paket sehen nur echte Aufgaben. Ein Vorschlag trägt Regel, Begründung, Belege, Anker und Entscheidung. Diese Felder gehören nicht an Task.
Ein oder zwei Eingänge? Ein Eingang „Vorschläge“ für Regel-, Ketten- und Autopilot-Vorschläge. Zwei Arten: PLAN („Übernehmen“ legt eine Aufgabe mit Termin an) und DO („Erfassen“ öffnet das Formular; der Vorschlag erledigt sich, sobald der Datensatz da ist).
Woher kommen Titel und Begründung? Aus reasonCode plus evidence (nur IDs, Zahlen, Daten), beim Lesen serverseitig gerendert. So bleibt „vor 9 Tagen“ immer aktuell, und Texte lassen sich ohne Datenmigration verbessern.
Wie entsteht der Autopilot? Regeltabelle im Code (Abschnitt 8), Werte in AutomationRule.config. Täglicher Abgleich aller Völker im Job und sofortiger Abgleich je Volk in einem Handler nach jedem fachlichen Ereignis.
Woher kommt die Saison? Je Stand die weiteste von drei Quellen: eigene Meldung („Salweide blüht“), Schätzung aus der Grünlandtemperatursumme (GTS), Kalender-Rückfall. Feste GTS-Schwelle nur dort, wo der DWD sie definiert (200 °C). Spätere Schwellen lernt die App aus den eigenen Meldungen der Vorjahre, statt sie zu erfinden.
Wer holt die Wetterdaten? Eine Worker-Schleife. Der Worker darf schon heute über 443 ins Internet. Jobs bleiben ohne Internet (NetworkPolicy unverändert). Koordinaten gehen auf 0,01° gerundet hinaus.
Welche Wetterquelle? Inhaber-Entscheidung: freie Open-Meteo-API, Risiko der Nutzungsbedingungen (nur nicht-kommerziell) bewusst übernommen. Zugriff nur über den Adapter WeatherHistorySource; Wechsel zu bezahltem Open-Meteo per Einstellung, zu DWD/Bright Sky per neuem Adapter plus Einstellung (6.3).
Volksstärke Besetzte Waben, 0–40, an Volkskarte und Durchsicht gleich. Neues Feld Colony.occupiedFrames. Der alte Wert Colony.strength (Skala 1–10) bleibt unverändert gespeichert und heißt in der Oberfläche „Volksstärke (alt, Skala 1–10)“. Keine Umrechnung (4.14).
Erinnerungen Je fällige Aufgabe eine Meldung und je Nutzer eine Morgenübersicht (Inhaber-Entscheidung). Beides in der App und, wenn der Nutzer es einschaltet, über Telegram.
Telegram-Zustellung Outbox NotificationDelivery wie WebhookDelivery. Handler und Jobs schreiben nur Zeilen. Eine Worker-Schleife sendet. Eine zweite empfängt /start und /stop per Long Polling, also ohne öffentlichen Eingang (Abschnitt 9a).
Alter einer Königin Eine Schwelle: queenMaxAgeYears = 2 für Dashboard (heute 3), QueenAged und Autopilot. Ab dem zweiten vollen Jahr nach dem Schlupf steht die Königin in ihrer dritten Saison. Wer dann warnt, hat in dieser Saison noch Zeit für die Nachzucht. Bei 3 käme der Hinweis erst in der vierten Saison. Der Wert ist eine Annahme nach Imkerpraxis und einstellbar.
Durchsicht → Volkskarte Im selben Request wie die Durchsicht, über ColoniesService.updateIn(tx, …). Atomar, offline-fähig, ein Hauptknopf „Speichern“. Kein zweiter Dialog nach dem Speichern.
Futterzustand Ein Vokabular: Inspection.feedStatus wird ColonyCondition, wie an der Volkskarte.
Menge der Honigcharge HoneyBatch.amount bleibt der aktuelle Rest, damit API und Shop stabil bleiben. Neu: initialAmount und Buchungen im Bewegungsbuch. Jede Änderung von amount läuft nur über HoneyBatchLedgerService.
Wartezeit und Plausibilität Warnungen in der Antwort plus eine Meldung je Konflikt. Nie eine Sperre (Annahme 10). Einzige Ausnahme: Wassergehalt über der gesetzlichen Grenze verlangt beim Abfüllen eine Bestätigung (§10).
Benachrichtigungen je Nutzer Bewusste Abweichung von Rückgrat-Spec §4 (Checkliste „NotificationRead je Nutzer“): je Nutzer eine eigene Zeile (Fan-out) mit Rechteprüfung beim Schreiben, keine Tabelle NotificationRead. Grund: Heute sieht jeder angemeldete Nutzer jede Rundmeldung (userId = null), auch ohne Leserecht auf Völker oder Varroa. Eine Lese-Tabelle würde nur das gemeinsame read-Flag lösen, nicht dieses Leck. Fan-out löst beides in einem Schritt, braucht keinen Join beim Lesen und ist zugleich die Grundlage für die Zustellung je Nutzer (Telegram).
Rechte Vorschläge nutzen die Aufgaben-Rechte (tasks:read/create/update). Neue Gruppe „Pflanzenentwicklung“ (phenology:read/create/update). Inhalte werden nach Leserechten gefiltert (Abschnitt 12).
Nachzügler des Rückgrats queens, extractions und honey-batches stellen auf Ereignisse um, soweit T4 sie anfasst. Damit entfallen ihre Altaufrufe webhooks.dispatch und calendarSync.enqueue.

2. Umfang

2.1 Im Umfang (§10 T4 → Lösung)

Punkt aus §10 T4 Lösung Abschnitt
Durchsicht → Volkskarte, einzeln abwählbar, über ColoniesService.update Abschnitt „Volkskarte“ im Durchsichtsformular, applyToColony im selben Request 7.9
Ein Vokabular für den Futterzustand Inspection.feedStatus → ColonyCondition, Migration 4.4
Varroa-Ampel nach Jahreszeit (LWG), Jungvölker, nach Maßnahme 14 Tage Bänder je Methode, Monat, Volkstyp; Regel VARROA_THRESHOLD erweitert 7.1
Fälligkeit der Durchsicht je Volk, Heute-Ansicht, §30 Intervall je Saisonabschnitt, Karte „Durchsicht fällig“ mit „Runde starten“ 7.2, 11
Wartezeit-Kette: Spanne im Kalender, „Volk erntefähig“, Prüfung bei Ernte/Schleuderung/Abfüllung WithdrawalCheckService, Warnungen, Meldungen, Kalenderquelle WITHDRAWAL 7.5
Ernte → Schleuderung → Charge vorbelegt, Plausibilität Vorbelegungs-Endpunkte, Warnungen 7.6
HoneyBatch.amount nur über das Bewegungsbuch initialAmount, BatchMovement.honeyBatchId, Ledger 7.7
Qualität vor der Abfüllung: Wassergehalt, MHD, Glasgröße Regel HONEY_WATER_CONTENT, Product.shelfLifeMonths 7.8
Biotechnik-Fristen Ketten-Vorschläge QUEEN_CELLS_SWARM_CHECK, QUEEN_ACCEPTANCE_CHECK, SPLIT_SCHEDULE 7.4
Volk beendet: Aufgaben anbieten, Königin-Zuordnung beenden (auch Auswinterung) gemeinsamer Helfer releaseQueenIn, Aufräum-Vorschlag 7.10
Saison-Autopilot: nächste 3 Schritte mit Begründung, GTS oder Meldung, kein ML, täglich Regeln A1–A10, Abgleich, Rangfolge 8
Vorschlagsliste mit „Übernehmen“/„Verwerfen“ Modell Suggestion, Seite „Vorschläge“ 9, 11.3
§11 T4-Befunde: Automatik-Aufgaben fehlen im Kalender; Vorlagen nur beim Lesen; ein read-Flag; amount am Bewegungsbuch vorbei; kein Grenzwert Wassergehalt Kalender und Vorlagen löst das Rückgrat (Tasks 9, 10, 15). T4 löst die übrigen drei. 4.6, 4.8, 7.8
Inhaber-Antwort 1: Volksstärke in besetzten Waben Colony.occupiedFrames, alter Wert bleibt 4.14
Inhaber-Antwort 3: Erinnerungen je Aufgabe und Morgenübersicht, in der App und per Telegram Handler notifications.task-due, Job-Schritt digest, Outbox NotificationDelivery, Telegram-Schleifen 9a

2.2 Befunde aus dem Code, die T4 behebt

Befund Lösung Abschnitt
Rundmeldungen (userId = null) sieht jeder Nutzer, und alle teilen ein read-Flag Fan-out je Nutzer mit Rechteprüfung; bewusste Abweichung von Rückgrat-Spec §4 4.8
Stirbt ein Volk bei der Auswinterung (markColonyDeceased) oder wird es über „Bearbeiten“ auf SOLD/DECEASED gesetzt, bleibt die Königin zugeordnet. dissolve und merge beenden die Zuordnung schon. ein Helfer releaseQueenIn für alle vier Wege, in derselben Transaktion 5.4, 7.10
Alter der Königin: Dashboard warnt ab 3 Jahren, QueenAged (Rückgrat) ab 2 eine Schwelle queenMaxAgeYears = 2 für alle Entscheidungen, 6.2
ARCHITECTURE hat die Annahmen 22 und 23 schon belegt, der T3-Plan plant 22–27 Neue Annahmen nur unter der „nächsten freien Nummer“ zum Zeitpunkt des Baus 15

2.3 Nicht im Umfang

  • Wanderung, Stockwaage, Sensoren (Inhaber-Entscheidung; T7).
  • Tierarzneimittel-Katalog, Etikett-Pflichtangaben, Behördenpaket (T6).
  • Bienenjahr-Rad, Waben-Standkarte, Lebenslinie (T5).
  • Mischungen, Importhonig, Backhonig-Logik (nur eigener deutscher Honig).
  • Web-Push und E-Mail. Telegram ist der einzige zusätzliche Kanal.
  • Telegram-Befehle außer /start, /stop und /hilfe. Kein Erfassen oder Abfragen über den Bot.
  • Drohnenrahmen-Regeln, Ertragsprognose, Änderungen an der Trachtprognose.
  • Bestand an Rohhonig vor der Charge (Ernte bis Schleuderung wird nicht gebucht).

2.4 Abgrenzung zu T3 und Rückgrat

Baustein Liefert T4 ändert
T3 Task 2 GET /varroa-measurements/threshold, varroaThresholdOf eine Schwelle (Milben/Tag) ersetzt durch Bänder (/thresholds); varroaThresholdOf liest das neue Config-Format
T3 Task 11 varroaLight(rate, threshold) gelb ab halber Schwelle varroaLight(value, band) mit Bändern vom Server
T3 Task 14 colonyStatusLight, INSPECTION_STALE_DAYS = 14 feste 14 Tage gelb ab Überschreiten von inspectionDueOn
T3 Task 3 ColonyOverviewRow.openWarnings/openCritical aus Benachrichtigungen aus offenen Vorschlägen (Priorität HIGH/URGENT)
T3 Task 9 Durchsichtsformular, takeover.ts „Letztes Mal“, Freitext-Futter Abschnitt „Volkskarte“; Freitext-Sonderfall entfällt nach Migration
T3 Task 3 ColonyOverviewRow.strength, OfflinePackV1.colonies alte Skala 1–10 zusätzlich occupiedFrames; strength bleibt als Altwert
T3 Task 1/9 Inspection.colonyStrength (besetzte Waben, @Min(0)) ohne Obergrenze @Max(40), wie an der Volkskarte
Rückgrat Task 10 triggerActionIn Aufgabe über createInTx Vorschlag über SuggestionsService.proposeIn
Rückgrat Task 18 INSPECTION_INTERVAL_DAYS = 21, ACTIVE_SEASON_MONTHS fester Takt Intervall je Saisonabschnitt (7.2)

3. Begriffe

  • Vorschlag (Suggestion): eine Empfehlung zu genau einem Objekt (meist ein Volk). Art PLAN oder DO.
  • Schritt (NextStep): ein Eintrag in „die nächsten 3 Schritte“. Quelle ist eine offene Aufgabe oder ein offener Vorschlag des Volks.
  • Anker: die Sache, an der ein Vorschlag hängt, z. B. die ID der letzten Durchsicht oder das Jahr. Ein verworfener Vorschlag kommt für denselben Anker nicht wieder.
  • Phase (PhenologyPhase): Stand der Pflanzenentwicklung, z. B. KIRSCHE.
  • Saisonabschnitt (SeasonSegment): aus der Phase abgeleitet, z. B. SCHWARMZEIT. Er bestimmt Intervalle und Regeln.
  • Heute: Kalendertag in Europe/Berlin (berlinToday(), neuer Helfer in src/common/dates.ts). Der Job läuft um 04:30 Berliner Zeit; der UTC-Tag von dailyDedupeKey ist dann derselbe Kalendertag.

4. Datenmodell

Alle Namen ladungstragend. Datumswerte wie bisher als DateTime (Mitternacht UTC), wie Task.dueDate.

4.1 Vorschläge

enum SuggestionStatus { OPEN ACCEPTED DISMISSED RESOLVED EXPIRED OBSOLETE }
enum SuggestionMode   { PLAN DO }
enum SuggestionSource { RULE CHAIN AUTOPILOT }
enum SuggestionIntent {
  INSPECT SWARM_CHECK QUEEN_CHECK QUEEN_ACCEPTANCE QUEEN_REPLACE SPLIT_PLAN
  VARROA_MEASURE VARROA_RECHECK VARROA_TREAT SUMMER_TREATMENT
  FEED HONEY_SUPER WINTER_CHECK HEALTH_CHECK FOLLOW_UP STOCK_REORDER CLEANUP
}

model Suggestion {
  id                 String                @id @default(cuid())
  ruleKey            AutomationTriggerType
  source             SuggestionSource
  mode               SuggestionMode
  intent             SuggestionIntent
  status             SuggestionStatus      @default(OPEN)
  priority           TaskPriority          @default(NORMAL)
  relatedType        TaskRelatedType
  relatedId          String?
  colonyId           String?
  colony             Colony?               @relation(fields: [colonyId], references: [id])
  reasonCode         String
  evidence           Json                  // nur IDs, Zahlen, YYYY-MM-DD
  requiredPermission String?               // z. B. 'varroa:read'; fehlt das Recht, ist der Vorschlag unsichtbar
  dueDate            DateTime?
  expiresAt          DateTime?
  plan               Json?                 // PLAN mit mehreren Terminen: [{ step, offsetDays, windowDays? }]
  dedupeKey          String?               @unique
  sourceEventId      String?
  decidedAt          DateTime?
  decidedByUserId    String?
  decidedByUser      User?                 @relation("SuggestionDecider", fields: [decidedByUserId], references: [id])
  decisionNote       String?
  createdAt          DateTime              @default(now())
  updatedAt          DateTime              @updatedAt

  tasks Task[]

  @@index([status, colonyId])
  @@index([status, dueDate])
  @@index([ruleKey, relatedType, relatedId, status])
}
  • Task bekommt sourceSuggestionId String? (Relation, Index). sourceAutomationTrigger bleibt und wird beim Übernehmen gesetzt.
  • dedupeKey ist nullbar und @unique, wie DomainEvent.dedupeKey. Kein partieller Index (Rückgrat-Spec §3.5).
  • Format: <ruleKey>:<relatedId>:<anker>, z. B. INSPECTION_INTERVAL:col_1:insp_9.

4.2 Pflanzenentwicklung

enum PhenologyPhase { SALWEIDE KIRSCHE APFEL HOLUNDER LINDE TRACHTENDE HERBST WINTER }
enum SeasonSegment  { WINTERRUHE FRUEHJAHR SCHWARMZEIT SOMMER SPAETSOMMER HERBST }
enum PhaseSource    { REPORT GTS CALENDAR }

model PhenologyReport {            // eigene Meldung per Antippen
  id        String         @id @default(cuid())
  apiaryId  String?                // null = alle Stände
  apiary    Apiary?        @relation(fields: [apiaryId], references: [id])
  phase     PhenologyPhase
  date      DateTime
  notes     String?
  userId    String
  user      User           @relation(fields: [userId], references: [id])
  revokedAt DateTime?              // zurückgezogen statt gelöscht
  createdAt DateTime       @default(now())
  updatedAt DateTime       @updatedAt
  @@index([date])
  @@index([apiaryId, phase])
}

model ApiaryPhenology {            // abgeleiteter Zustand, schreibt nur der Job/Handler
  apiaryId       String
  year           Int
  gts            Float?
  gtsThroughDay  DateTime?
  missingDays    Int             @default(0)
  estimatedPhase PhenologyPhase?
  reportedPhase  PhenologyPhase?
  effectivePhase PhenologyPhase?
  phaseSource    PhaseSource?
  segment        SeasonSegment
  segmentSince   DateTime
  updatedAt      DateTime        @updatedAt
  @@id([apiaryId, year])
}

model ApiaryWeatherDay {           // Tagesmittel je Stand, schreibt nur die Worker-Schleife
  apiaryId  String
  day       DateTime               // Kalendertag Europe/Berlin
  meanTempC Float?
  source    String                 // 'forecast' | 'archive'
  fetchedAt DateTime @default(now())
  @@id([apiaryId, day])
}

4.3 Ableger

  • enum SplitQueenSource { RAISE_OWN QUEEN_CELL QUEEN } (Nachschaffung, reife Weiselzelle, Königin zugesetzt).
  • ColonyLineage.queenSource SplitQueenSource?. null heißt „unbekannt“. Dann gibt es keine Kette, nichts wird angenommen.
  • SplitColonyDto bekommt date? (heute: fest new Date()) und queenSource?.

4.4 Futter-Vokabular

  • Inspection.feedStatus String? → ColonyCondition?. Volkskarte und Durchsicht sprechen dieselbe Sprache.

  • Abbildung (ein Helfer feedConditionFromLegacy in src/common/feed-vocabulary.ts, auch für den Backup-Import alter Bundles):

    alter Wert (getrimmt, ohne Groß-/Kleinschreibung) neu
    ausreichend GOOD
    niedrig WEAK
    kritisch CRITICAL
    leer null
    jeder andere Text null, Text wandert an das Ende von notes als „Futter (alt): …“
  • Labels für Futter überall gleich (FEED_CONDITION_LABELS): EXCELLENT „Reichlich“, GOOD „Ausreichend“, AVERAGE „Mittel“, WEAK „Niedrig“, CRITICAL „Kritisch“.

  • Das Durchsichtsformular bietet wie bisher drei Werte plus „Nicht geprüft“. Ein gespeicherter Wert außerhalb dieser drei erscheint im Bearbeiten-Dialog trotzdem.

4.5 Varroa-Probe

  • VarroaMeasurement.sampleBees Int? (Probengröße in Bienen).
  • Bei POWDERED_SUGAR und WASHING_METHOD wird calculatedValue = miteCount / sampleBees × 100 (Befall in %), sonst wie bisher. Die Regel für manuelle Werte (Annahme 12) gilt unverändert.
  • Das Formular bietet den Chip „500 Bienen (≈ 50 g)“, vorgewählt ist nichts (wie T3 bei den Tagen).

4.6 Bewegungsbuch der Honigcharge

  • HoneyBatch.initialAmount Float? (kg, Ausgangsmenge).
  • BatchMovement.honeyBatchId String? (FK, onDelete: Restrict) und BatchMovement.sourceBottlingBatchId String? (FK, onDelete: SetNull, Rückverweis auf die verursachende Abfüllung).
  • BatchMovementType + BOTTLING.
  • quantity ist bei Honig in Gramm (Int), mit Vorzeichen.
  • CHECK-Constraint neu: num_nonnulls("bottlingBatchId","productionBatchId","rawMaterialLotId","honeyBatchId") = 1.
  • Invariante: amount = initialAmount + Σ quantity / 1000, gerundet auf Gramm. Ist initialAmount null, wird nicht gebucht (Menge nicht erfasst, wie heute).

4.7 Produkt

  • Product.shelfLifeMonths Int? (Haltbarkeit in Monaten, kein Standardwert). packagingSize gilt als Gramm wie BottlingBatch.jarSize; das prüft der Bau am Produktformular.

4.8 Benachrichtigungen

Bewusste Abweichung von Rückgrat-Spec §4: Fan-out statt NotificationRead (Begründung in den Entscheidungen).

  • Notification.userId wird Pflicht. Rundmeldungen (userId = null) gibt es nicht mehr.
  • Notification.dedupeKey String? @unique. Format <art>:<objekt…>:<userId>, bei täglichen Meldungen mit Tag, z. B. withdrawal-ended:<treatmentId>:<userId> oder job-failed:<job>:<YYYY-MM-DD>:<userId>.
  • Notification.category NotificationCategory (neu): TASK_DUE, DIGEST, SUGGESTION, WITHDRAWAL, SEASON, SYSTEM. Altzeilen bekommen SUGGESTION (Automatik) bzw. SYSTEM (Job-Fehler).
  • Notification.link String?: relativer App-Pfad (beginnt mit /, kein //, kein Schema), z. B. /colonies/<id>. Wird beim Schreiben geprüft.
  • Notification.requiredPermissions String[] @default([]): alle Leserechte, die der Inhalt voraussetzt (die Morgenübersicht nutzt mehrere). Der Telegram-Versand prüft sie beim Senden erneut (9a.6).
  • NotificationsService.record verlangt userId. Neu recordForUsers(userIds, input, tx); recordForPermission bleibt der Normalweg und vergibt den Schlüssel je Empfänger. Beide legen in derselben Transaktion die Telegram-Zustellungen an (9a.4).

4.9 Kalender

  • CalendarSyncSourceType + WITHDRAWAL (Quelle: Behandlungs-ID).
  • CalendarSyncSettings.syncWithdrawal Boolean @default(true).
  • GraphCalendarEventInput bekommt optional endDate (ganztägig, mehrtägig; Graph-Ende exklusiv).

4.10 Rechte

  • Neue Gruppe „Pflanzenentwicklung“, Ressource phenology: read „Ansehen“, create „Melden“, update „Bearbeiten“.
  • Die Migration vergibt sie wie bei winter-checks an Nicht-System-Rollen (Annahme 16): read an Rollen mit colonies:read, create/update an Rollen mit inspections:create.

4.11 Regeln (AutomationTriggerType)

Neue Werte, alle mit Eintrag in DEFAULT_RULES, Config-Parser und Texten:

Trigger Art Standard aktiv Config (Standard)
INSPECTION_INTERVAL Autopilot-Intervall ja Tage je Abschnitt, siehe 7.2
QUEEN_CELLS_SWARM_CHECK Kette ja { dueInDays: 7 }
QUEEN_ACCEPTANCE_CHECK Kette ja { dueInDays: 7 }
SPLIT_SCHEDULE Kette ja Schritte je queenSource, siehe 7.4
COLONY_ENDED_CLEANUP Kette ja {}
WITHDRAWAL_ENDED Meldung ja { notify: true }
HONEY_WATER_CONTENT Qualität ja siehe 7.8
SEASON_AUTOPILOT Autopilot ja siehe 8.2
PHENOLOGY Saison ja siehe 7.3
  • VARROA_THRESHOLD behält seinen Namen, die Config wird zum Bänder-Format (7.1). Ein alter Wert thresholdMitesPerDay wird als naturalFall.redAbove gelesen.
  • Jede Regel bekommt einen Parser parse<Trigger>Config(json) in src/automation/rule-config/. Ungültige Werte fallen auf den Standard zurück, wie varroaThresholdOf. PATCH /automation/rules/:id lehnt ungültige bekannte Schlüssel mit 400 ab.
  • Bestehende vorschlagende Regeln bekommen config.notify (Standard: true für QUEENLESS_NO_EGGS, VARROA_THRESHOLD, COLONY_HEALTH_CRITICAL; sonst false).

4.12 Migrationen

Zeitstempel vergibt der Bau. Die Reihenfolge ist bindend. Enum-Werte kommen in eigene Migrationen vor jeder Datenänderung, die sie benutzt: Postgres kann einen neuen Enum-Wert erst nach dem Commit verwenden.

Nr Name Inhalt Prüfung danach
1 <ts>_t4_enum_values AutomationTriggerType (9 Werte), BatchMovementType.BOTTLING, CalendarSyncSourceType.WITHDRAWAL migrate diff --exit-code
2 <ts>_t4_suggestions Enums und Suggestion, Task.sourceSuggestionId Wächter Backup
3 <ts>_t4_feed_vocabulary neue Spalte, Abbildung nach 4.4, Freitext nach notes, alte Spalte weg, Umbenennen SQL: Zahl umgezogener Freitexte im Migrationslog; keine Zeile mit Text und ohne Notiz
4 <ts>_t4_phenology Enums, drei Modelle, Rechte und Rollen-Zuordnung Rechte-Abdeckung e2e
5 <ts>_t4_split_queen_source SplitQueenSource, ColonyLineage.queenSource –
6 <ts>_t4_honey_ledger Spalten, CHECK neu, Rückrechnung (siehe unten) Invarianten-Abfrage liefert 0 Zeilen
7 <ts>_t4_quality_fields VarroaMeasurement.sampleBees, Product.shelfLifeMonths Null-Regel-Spec
8 <ts>_t4_notification_fanout Rundmeldungen je aktivem Nutzer kopieren (mit gleichem read), Originale löschen, userId NOT NULL, dedupeKey keine Zeile mit userId IS NULL
9 <ts>_t4_calendar_withdrawal syncWithdrawal –
10 <ts>_t4_occupied_frames Colony.occupiedFrames Int? mit CHECK 0–40; keine Datenänderung an strength Prüfabfrage: keine Inspection.colonyStrength über 40 (sonst vor dem Deploy klären, nicht kürzen)
11 <ts>_t4_notification_channels NotificationCategory, Notification.category/link/requiredPermissions, NotificationPreference, TelegramLink, TelegramLinkToken, NotificationDelivery, TelegramPollState Wächter Backup; migrate diff --exit-code

Migration 8 läuft vor 11. Migration 11 belegt category der Altzeilen nach der Regel aus 4.8 und setzt danach NOT NULL.

Rückrechnung in Migration 6, je Charge mit amount IS NOT NULL:

  1. initialAmount = amount + Σ totalAmount aller Abfüllungen der Charge.
  2. Je Abfüllung eine Buchung BOTTLING mit −round(totalAmount × 1000) g, sourceBottlingBatchId, createdAt der Abfüllung, Notiz „Übernahme aus Abfüllung (T4)“.

Frühere Handkorrekturen von amount stecken danach in initialAmount. Das steht in der Notiz der Charge nicht; es steht in ARCHITECTURE (Annahme „Bewegungsbuch der Honigcharge“).

4.13 Backup

Modell Backup Begründung (englisch in MODELS_EXCLUDED_FROM_BACKUP)
Suggestion ja Entscheidungen des Imkers; ohne sie kämen verworfene Vorschläge nach einem Restore wieder
PhenologyReport ja eigene Beobachtungen
ApiaryPhenology nein 'Derived season state, recomputed by daily-evaluation.'
ApiaryWeatherDay nein 'Weather cache, refetched by the worker.'
NotificationPreference ja Einstellungen des Nutzers
TelegramLink nein 'Chat binding to this deployment's bot - users re-link after a restore (like ApiKey).'
TelegramLinkToken nein 'Single-use link tokens, valid for minutes.'
NotificationDelivery nein 'Delivery queue - restoring it would resend old messages.'
TelegramPollState nein 'Update offset and poll lease of this deployment.'

Der Import alter Bundles bildet ab: Futter-Freitext (4.4), Notification ohne userId (Fan-out wie Migration 8) und ohne category (wie Migration 11), HoneyBatch ohne initialAmount (Rückrechnung wie Migration 6). Colony.strength und Colony.occupiedFrames werden unverändert übernommen.

4.14 Volksstärke (Inhaber-Antwort 1)

  • Neu: Colony.occupiedFrames Int?, Einheit besetzte Waben, 0–40. Dieselbe Einheit und dieselben Grenzen wie Inspection.colonyStrength (dort kommt @Max(40) hinzu).
  • Alt: Colony.strength (1–10, ohne Einheit) bleibt mit allen Werten, wie sie sind. Keine Umrechnung: Eine „7 von 10“ lässt sich nicht verlässlich in Waben übersetzen.
  • Bedeutung ab T4:
    • strength heißt in der Oberfläche „Volksstärke (alt, Skala 1–10)“. Das Bearbeiten-Formular zeigt es nur, wenn ein Wert gespeichert ist (wie „Brutmenge (alt)“ in T3). Leeren geht (Null-Regel), Ändern geht im Bereich 1–10.
    • Neue Völker bekommen im Formular nur occupiedFrames. Die API nimmt strength weiter an, damit nichts bricht; Swagger markiert es als veraltet.
    • Anzeige auf der Volksseite: „{n} besetzte Waben“. Fehlt der neue Wert, aber der alte ist da: „Volksstärke (alt): 7 von 10“.
  • Historie bleibt lesbar: Audit-Einträge und Exporte behalten die alten Werte mit ihrem alten Feldnamen. CSV-Export/-Import und Backup führen beide Spalten.
  • Der Autopilot (A7) und die Statistik „Entwicklung Volksstärke“ nutzen die Durchsichten (colonyStrength), nicht die Volkskarte.

4.15 Benachrichtigungskanäle (Inhaber-Antwort 3)

enum NotificationCategory   { TASK_DUE DIGEST SUGGESTION WITHDRAWAL SEASON SYSTEM }
enum NotificationChannel    { TELEGRAM }
enum NotificationDeliveryStatus { PENDING PROCESSING DELIVERED SKIPPED FAILED }

model NotificationPreference {      // je Nutzer, Standard beim ersten Lesen angelegt
  userId             String   @id
  user               User     @relation(fields: [userId], references: [id])
  taskDueEnabled     Boolean  @default(true)    // Meldung je fälliger Aufgabe
  digestEnabled      Boolean  @default(true)    // Morgenübersicht
  digestTime         String   @default("06:30") // HH:MM, Europe/Berlin, 05:00–12:00
  quietFrom          String?  @default("21:00") // Ruhezeit für Telegram; null = keine
  quietUntil         String?  @default("06:30")
  telegramEnabled    Boolean  @default(false)
  telegramCategories NotificationCategory[] @default([TASK_DUE, DIGEST])
  telegramDetail     Boolean  @default(true)    // false: nur „Neue Meldung in ApiCore“ plus Link
  updatedAt          DateTime @updatedAt
}

model TelegramLink {
  userId            String   @id
  user              User     @relation(fields: [userId], references: [id])
  chatId            BigInt   @unique           // privater Chat; ein Chat gehört zu genau einem Nutzer
  telegramFirstName String?                    // nur zur Anzeige „Verbunden mit …“
  linkedAt          DateTime @default(now())
}

model TelegramLinkToken {
  id         String    @id @default(cuid())
  userId     String
  user       User      @relation(fields: [userId], references: [id])
  tokenHash  String    @unique                 // SHA-256 des Tokens, nie der Klartext
  expiresAt  DateTime                          // 10 Minuten
  consumedAt DateTime?
  createdAt  DateTime  @default(now())
  @@index([userId])
}

model NotificationDelivery {        // Outbox wie WebhookDelivery
  id             String                     @id @default(cuid())
  channel        NotificationChannel
  userId         String?                    // gesetzt bei Meldungen; Chat wird beim Senden aus TelegramLink gelesen
  user           User?                      @relation(fields: [userId], references: [id])
  chatId         BigInt?                    // nur für Systemantworten an einen Chat (auch unverbundene)
  notificationId String?
  notification   Notification?              @relation(fields: [notificationId], references: [id], onDelete: Cascade)
  templateCode   String?                    // Systemantworten des Bots (Verbunden, Ungültig, Abgemeldet, Hilfe)
  status         NotificationDeliveryStatus @default(PENDING)
  attempts       Int                        @default(0)
  availableAt    DateTime                   @default(now())
  claimedAt      DateTime?
  lastStatusCode Int?
  lastError      String?
  deliveredAt    DateTime?
  createdAt      DateTime                   @default(now())
  @@index([status, availableAt])
  @@index([userId, createdAt])
}

model TelegramPollState {           // eine Zeile: Offset und Lease des Empfängers
  id          String    @id @default("singleton")
  updateOffset BigInt   @default(0)
  leaseOwner  String?
  leaseUntil  DateTime?
  lastPollAt  DateTime?
  lastError   String?
}
  • Der Text einer Zustellung wird nicht gespeichert. Der Versand rendert ihn aus der Notification bzw. aus templateCode (9a.6).
  • CHECK-Constraint: genau eins von (notificationId mit userId) oder (templateCode mit chatId).
  • Ruhezeit und Morgenzeit sind Annahmen (Standard 21:00–06:30 bzw. 06:30) und je Nutzer änderbar.
  • chatId ist kein Geheimnis (ohne Bot-Token nutzlos), aber personenbezogen. Darum nicht im Backup und nie in Logs.

5. Ereignisse und Handler

5.1 Neue Ereignisse

T4 erweitert DomainEventPayloads bewusst. Der Kommentar „vollständig“ im Rückgrat-Plan meint dessen Stufe.

Ereignis Payload Aggregat Erzeuger
ColonySplit { parentColonyId, lineageId, queenSource: SplitQueenSource | null, splitOn } Colony (Ableger) ColoniesService.split
QueenAssigned { colonyId, historyId, assignedOn, previousColonyId: string | null } Queen QueensService.assignColony
ExtractionRecorded / ExtractionCorrected { harvestIds: string[] } Extraction ExtractionsService
HoneyBatchRecorded / HoneyBatchCorrected { extractionId } HoneyBatch HoneyBatchesService
PhenologyReported / PhenologyCorrected { apiaryId: string | null, phase, reportedOn } PhenologyReport PhenologyService
SeasonPhaseChanged (zeitbasiert) { apiaryId, phase, segment, previousPhase, previousSegment, source } Apiary daily-evaluation, Handler phenology.recompute
  • AggregateType + Extraction, HoneyBatch, PhenologyReport, Apiary.
  • SeasonPhaseChanged hat dedupeKey = dailyDedupeKey('SeasonPhaseChanged', apiaryId, Tag).
  • Payloads tragen nur IDs, Enum-Werte und Daten. Keine Namen, keine Notizen.

5.2 Handler

Ereignis Erzeuger Handler Wirkung
InspectionRecorded inspections automation.inspection-rules (Rückgrat, geändert) QUEENLESS_NO_EGGS, FEED_STATUS_CRITICAL (jetzt feedStatus = CRITICAL) → Vorschlag statt Aufgabe
InspectionRecorded inspections chains.queen-cells queenCellsPresent = true → PLAN „Schwarmkontrolle“, fällig +7
InspectionRecorded, InspectionCorrected inspections autopilot.colony-sync Schritte des Volks neu abgleichen
VarroaMeasured, VarroaCorrected varroa automation.varroa-threshold (geändert) Band rot → PLAN „Behandlung prüfen“; nicht bis 14 Tage nach Behandlungsende
VarroaMeasured, VarroaCorrected varroa autopilot.colony-sync –
TreatmentRecorded, TreatmentCorrected treatments automation.treatment-follow-up (Rückgrat), calendar-sync.withdrawal (neu), autopilot.colony-sync Wartefenster in die Kalender-Outbox
FeedingRecorded, FeedingCorrected feedings automation.feeding-follow-up (Rückgrat), autopilot.colony-sync –
HarvestRecorded, HarvestCorrected harvests withdrawal.harvest-check, autopilot.colony-sync (je Volk) WARNUNG je Konflikt (Volk, Ernte), dedupliziert
ExtractionRecorded, ExtractionCorrected extractions withdrawal.extraction-check Meldung nur für noch nicht gemeldete Konflikte
HoneyBatchRecorded honey-batches webhooks.forward Webhook batch.created (bisher Altpfad)
BatchBottled, BottlingCorrected bottling withdrawal.bottling-check wie oben
ColonyStatusChanged colonies automation.colony-health (Rückgrat, geändert), autopilot.colony-sync KRITISCH → Vorschlag
ColonyEnded colonies, winter-checks suggestions.colony-ended offene Vorschläge → OBSOLETE; Aufräum-Vorschlag (7.10)
ColonySplit colonies chains.split-schedule, autopilot.colony-sync (beide Völker) PLAN mit Terminen je queenSource
QueenAssigned queens chains.queen-acceptance, calendar-sync.queen-assignment, webhooks.forward, autopilot.colony-sync PLAN „Annahme prüfen“ +7; Kalender und Webhook queen.replaced statt Altpfad
TaskCompleted, TaskUpdated tasks autopilot.colony-sync (nur relatedType = COLONY) –
WithdrawalEnded daily-evaluation notifications.withdrawal-ended, autopilot.colony-sync INFO „B3 ist wieder erntefähig“ an harvests:create, nur wenn kein anderes Fenster mehr läuft
QueenAged, InspectionOverdue, VarroaMeasurementDue daily-evaluation autopilot.colony-sync –
TaskDue daily-evaluation notifications.task-due HINWEIS „Heute fällig: {Titel}“ mit Link an den Zuständigen, ohne Zuständigen an Inhaber von tasks:complete; nur bei taskDueEnabled; Schlüssel task-due:<taskId>:<Tag>:<userId>; Telegram-Zustellung nach 9a.4
PhenologyReported, PhenologyCorrected phenology phenology.recompute Zustand des Stands bzw. aller Stände neu; schreibt bei Wechsel SeasonPhaseChanged
SeasonPhaseChanged daily-evaluation, phenology autopilot.apiary-sync, notifications.season-segment alle Völker des Stands; INFO nur bei Wechsel des Abschnitts

5.3 Regeln für alle T4-Handler

  • Handler-IDs folgen HANDLER_ID_PATTERN. Alle stehen in Modulen, die WorkerModule importiert (Carry-forward Rückgrat Task 6).
  • Nur Schreiben über tx, keine Netzwerkaufrufe (Wächter handler-contract.spec.ts).
  • autopilot.colony-sync sperrt zuerst die Volkszeile (SELECT … FROM "Colony" WHERE id = $1 FOR UPDATE). Zwei Ereignisse desselben Volks in zwei Workern laufen so nacheinander.
  • Vorschläge entstehen über createMany({ skipDuplicates: true }) mit dedupeKey. Eine erneute Zustellung erzeugt nichts doppelt.
  • Ein Handler für ein beendetes Volk tut nichts (außer suggestions.colony-ended).

5.4 Erzeuger-Umstellungen

  • ColoniesService.updateIn(tx, id, dto, actingUserId) wird der gemeinsame Kern von update. Ihn nutzt auch die Durchsicht (7.9).

  • releaseQueenIn(tx, colonyId, endDate) in src/colonies/colony-lifecycle.ts: offene QueenColonyHistory mit endDate beenden, currentQueenId = null. Pflicht in jedem Weg, auf dem ein Volk endet:

    Weg heute T4
    dissolve (DISSOLVED) beendet die Zuordnung über den Helfer
    merge (MERGED) beendet die Zuordnung über den Helfer
    update auf SOLD oder DECEASED Königin bleibt zugeordnet über den Helfer, Enddatum = Tag der Änderung
    Auswinterung mit markColonyDeceased (DECEASED) Königin bleibt zugeordnet über den Helfer, Enddatum = Datum der Auswinterung

    Ein Wächter-Test prüft: Jede Stelle, die status auf einen Wert aus TERMINAL_COLONY_STATUSES setzt, ruft releaseQueenIn in derselben Transaktion auf.

  • ColoniesService.split schreibt ColonySplit, Audit in der Transaktion.

  • QueensService.assignColony schreibt QueenAssigned; webhooks.dispatch('queen.replaced') und calendarSync.enqueue('QUEEN_ASSIGNMENT') entfallen.

  • ExtractionsService und HoneyBatchesService: Audit in die Transaktion, Ereignisse wie in 5.1. webhooks.dispatch('batch.created') entfällt.

  • Wächter after-commit-usage.spec.ts: Die Liste der Altpfade verliert queens und honey-batches.

6. daily-evaluation und Wetter-Schleife

6.1 Ablauf des Jobs

Der Job aus Rückgrat Task 18 bekommt Schritte. Jeder Schritt läuft in eigenen Transaktionen. Scheitert einer, laufen die anderen; JobRun ist dann FAILED mit den Zählern.

Schritt Inhalt Transaktion
1 phenology je aktivem Stand GTS, Phasen, Abschnitt berechnen (7.3), ApiaryPhenology schreiben, bei Wechsel SeasonPhaseChanged je Stand
2 Zeit-Ereignisse WithdrawalEnded, TaskDue, QueenAged, VarroaMeasurementDue, InspectionOverdue mit den Änderungen aus 6.2 je Typ (wie Rückgrat)
3 autopilot je lebendem Volk AutopilotService.syncColonyIn(tx, colonyId, today) je Volk, Volkszeile gesperrt
4 suggestions-expiry OPEN mit expiresAt < today → EXPIRED eine Anweisung
5 digest je aktivem Nutzer mit digestEnabled eine Morgenübersicht (9a.3) samt Telegram-Zustellung zur eingestellten Uhrzeit je Nutzer

JobResult zusätzlich: phenologyApiaries, seasonChanges, autopilotColonies, suggestionsOpened, suggestionsResolved, suggestionsExpired, digests.

Schritt 5 läuft nach Schritt 3, damit die Übersicht die frischen Schritte kennt. TaskDue aus Schritt 2 verarbeitet der Worker gleich danach; die Meldungen je Aufgabe sind also vor der frühesten Übersichts-Uhrzeit (05:00) da.

6.2 Änderungen an den zeitbasierten Ereignissen

Ereignis Rückgrat heute T4
InspectionOverdue Intervall 21 Tage, nur März–Oktober Tag nach inspectionDueOn() (7.2); keine Ereignisse in WINTERRUHE
VarroaMeasurementDue Intervall 28 Tage, nur März–Oktober Regel A2 (8.2): 28 Tage außerhalb WINTERRUHE, dazu Winterfenster
QueenAged ab 2 Jahren, einmal im Jahr Schwelle aus SEASON_AUTOPILOT.queenMaxAgeYears (Standard 2, Begründung in den Entscheidungen). DashboardService nutzt dieselbe Zahl; die Konstante OLD_QUEEN_AGE_YEARS = 3 entfällt.
WithdrawalEnded (endDate ?? date) + withdrawalPeriodDays unverändert; Fensterrechnung aus dem gemeinsamen Helfer withdrawal-window.ts (auch für getWithdrawalStatus)

INSPECTION_INTERVAL_DAYS und ACTIVE_SEASON_MONTHS entfallen.

6.3 Worker-Schleife phenology-weather

  • Neue Zeile in der Loop-Tabelle von WorkerRuntime: Takt 60 min, kein Weckkanal, respektiert das Stop-Signal.

  • Je Takt: für jeden aktiven Stand mit Koordinaten fehlende Tage vom 1. Januar bis gestern (Europe/Berlin) suchen.

    • Fehlende Tage der letzten 90 Tage: Forecast-API mit past_days=92, daily=temperature_2m_mean, timezone=Europe/Berlin.
    • Ältere Lücken: Archiv-API mit start_date/end_date.
  • Koordinaten auf 0,01° gerundet. Stände mit demselben gerundeten Punkt teilen sich eine Anfrage.

  • Feste Hosts aus Konstanten, Timeout 8 s, Antwortgröße begrenzt, Werte nur zwischen −50 und 50 °C, sonst null.

  • Fehler: loggen über loggableError(), nächster Takt versucht es wieder. Kein Ereignis, keine Meldung.

  • cleanup löscht ApiaryWeatherDay älter als 4 Kalenderjahre (für die Kalibrierung, 7.3).

  • Quelle (Inhaber-Entscheidung): freie Open-Meteo-API. Laut Anbieter ist sie nur für nicht-kommerzielle Nutzung frei; der Inhaber trägt dieses Risiko bewusst. Unser Bedarf liegt weit unter der Tagesgrenze des Anbieters (eine Anfrage je gerundetem Punkt und Tag).

  • Adapter: Die Schleife kennt nur WeatherHistorySource.dailyMeans(point, fromDay, toDay): Promise<DailyMean[]>. Welche Umsetzung läuft, bestimmen Umgebungsvariablen aus der ConfigMap:

    Variable Standard Zweck
    WEATHER_HISTORY_SOURCE open-meteo Auswahl der Umsetzung
    OPEN_METEO_FORECAST_URL https://api.open-meteo.com/v1/forecast Basis-URL; für das bezahlte Angebot die Kunden-URL
    OPEN_METEO_ARCHIVE_URL https://archive-api.open-meteo.com/v1/archive dto
    OPEN_METEO_API_KEY leer Secret; nur für das bezahlte Angebot
    • Wechsel zu bezahltem Open-Meteo: nur Einstellungen (URL und Schlüssel), kein Code.
    • Wechsel zu DWD/Bright Sky: eine neue Umsetzung (etwa 0,5 PT), danach WEATHER_HISTORY_SOURCE=bright-sky.
    • Die bestehende Wetteranzeige (WeatherService) liest dieselben URL- und Schlüssel-Variablen. Ein Wechsel des Angebots gilt so für beide.
    • URLs nur https: und nur aus der Konfiguration, nie aus Nutzereingaben. Der Schlüssel erscheint nie in Logs oder lastError (URL wird vor dem Loggen entfernt).

7. Fachregeln

Alle Werte stehen in AutomationRule.config und sind in „Verwaltung > Automatisierung“ änderbar. „Annahme“ heißt: Standardwert ohne belastbare Quelle, vor dem Bau zu prüfen.

7.1 Varroa-Ampel nach Jahreszeit

Methode Zeitraum Grün Gelb Rot Quelle
Natürlicher Milbenfall (Milben/Tag) März–Oktober unter 5 5 bis 10 über 10 LWG Veitshöchheim, Tabelle aus Fahrplan §10; Gültigkeit ab März = Annahme
Natürlicher Milbenfall (Milben/Tag) November–Februar bis 0,5 – über 0,5 LWG (Fahrplan §10: „ab November Behandlung schon über 0,5“); Ende Februar = Annahme
Puderzucker (Milben je 100 Bienen, %) Mai–Juli unter 1 % 1–5 % über 5 % LWG „Juli“; Mai und Juni = Annahme
Puderzucker August–Oktober unter 2 % 2–5 % über 5 % LWG „September“; August und Oktober = Annahme
Puderzucker November–April keine Ampel –
Auswaschung wie Puderzucker Annahme (gleiche Befallsrate)
Drohnenbrut keine Ampel –
  • Grenzen: rot heißt Wert > redAbove. Gelb heißt Wert ≥ yellowFrom. Genau 10,0 Milben/Tag sind also gelb. Heute (T3) ist 10,0 rot; das ändert sich mit der Quelle.
  • Jungvölker (YOUNG_COLONY, NUCLEUS_COLONY, MATING_NUCLEUS): Schwellen des natürlichen Milbenfalls × 0,5 (Fahrplan §10). Nur beim Milbenfall, weil Prozentwerte schon auf die Probe bezogen sind (Annahme).
  • Nach einer Behandlung: Liegt das Messdatum bis 14 Tage nach endDate ?? date einer Behandlung, gibt es keine Ampel. Der Text lautet „Behandlungseffekt – Nachkontrolle ab {Datum}“. Die Regel schlägt dann nichts vor (Fahrplan §10).
  • Wirkung: Rot → PLAN-Vorschlag VARROA_TREAT „Behandlung prüfen“, Priorität HIGH, Anker = Mess-ID.
  • Config: { naturalFall: { seasonMonths, yellowFrom: 5, redAbove: 10, winterRedAbove: 0.5 }, infestationPercent: { summerMonths: [5,6,7], summer: { yellowFrom: 1, redAbove: 5 }, autumnMonths: [8,9,10], autumn: { yellowFrom: 2, redAbove: 5 } }, youngColonyFactor: 0.5, youngColonyTypes, recheckAfterDays: 14, notify: true }.
  • Ein Helfer varroa-bands.ts (bandsFor(date, colonyType, config), classify(value, band)) liefert Ampel, Endpunkt und Regel. Das Frontend klassifiziert nur mit dem gelieferten Band.

7.2 Fälligkeit der Durchsicht

inspectionDueOn(colony, lastInspection, segment, config) = Datum der letzten Durchsicht (sonst colony.createdAt) plus Intervall des heutigen Abschnitts. In WINTERRUHE null.

Abschnitt Intervall (Standard) erlaubt Quelle
FRUEHJAHR 14 Tage 7–35 Fahrplan §10 („sonst 14–21“)
SCHWARMZEIT 7 Tage 7–9 Fahrplan §10; Bieneninstitute (z. B. LLH Kirchhain, LWG): Weiselzellen sind nach 8–9 Tagen verdeckelt, darum Kontrolle im 7-Tage-Takt
SOMMER 14 Tage 7–35 Fahrplan §10
SPAETSOMMER 21 Tage 7–35 Fahrplan §10
HERBST 21 Tage 7–35 Fahrplan §10
WINTERRUHE keins – Winterruhe; Einwinterung und Auswinterung laufen über Regeln A9/A10
  • Erste Durchsicht im Jahr: Liegt die letzte Durchsicht vor dem Beginn von FRUEHJAHR, ist das Volk ab Abschnittsbeginn fällig. Die Begründung sagt „Erste Durchsicht im Jahr“.
  • Ausgabe: inspectionDueOn in ColonyOverviewRow (T3), in GET /colonies/:id/next-steps und in GET /autopilot/today. Das beantwortet §30 „Welche Völker wurden zu lange nicht kontrolliert?“.

7.3 Pflanzenentwicklung und Saisonabschnitt

Grünlandtemperatursumme (DWD): Summe der positiven Tagesmitteltemperaturen ab 1. Januar. Januar zählt mit Faktor 0,5, Februar mit 0,75, ab März voll. Ab 200 °C gilt die Vegetation als nachhaltig begonnen. Fehlen mehr als 10 % der Tage, gilt die Summe als lückenhaft und wird nicht benutzt (Text „Wetterdaten lückenhaft“).

Phasen:

Phase Meldung (ein Tipp) Bezug GTS-Schwelle Kalender-Rückfall
SALWEIDE „Salweide blüht“ Imkerpraxis: erste Pollentracht, Frühjahrsdurchsicht 200 °C (DWD-Vegetationsbeginn; gleichgesetzt mit der Salweidenblüte = Annahme) 20.03.
KIRSCHE „Kirsche blüht“ Imkerpraxis: Honigraum, Beginn der Schwarmkontrollen aus eigenen Meldungen der Vorjahre, sonst keine 15.04.
APFEL „Apfel blüht“ DWD-Vollfrühling aus eigenen Meldungen der Vorjahre, sonst keine 28.04.
HOLUNDER „Holunder blüht“ DWD-Frühsommer keine 30.05.
LINDE „Linde blüht“ DWD-Hochsommer keine 20.06.
TRACHTENDE „Tracht vorbei“ Imkerpraxis: letzte Ernte keine 01.08.
HERBST „Efeu blüht“ Imkerpraxis: letzte Tracht keine 15.09.
WINTER „Winterruhe“ brutarm bis brutfrei keine 15.11.

Alle Kalender-Rückfälle sind Annahmen für Schwaben (rund 450–500 m). Vor dem Bau gegen DWD-Phänologiedaten der Region prüfen.

Effektive Phase je Stand und Jahr, als Maximum dreier Werte:

  1. Gemeldet: höchste Phase aller nicht zurückgezogenen Meldungen dieses Jahres für den Stand oder für alle Stände.
  2. Geschätzt (GTS): höchste Phase mit Schwelle ≤ heutiger GTS, nur wenn die GTS nicht lückenhaft ist.
  3. Kalender: höchste Phase mit Rückfall-Datum ≤ heute, nur für Phasen ohne GTS-Schwelle oder ohne Wetterdaten am Stand.

phaseSource nennt die Quelle des Maximums. Innerhalb eines Jahres läuft die Phase nur vorwärts. Einzige Ausnahme: Eine zurückgezogene Meldung kann sie zurücksetzen.

Kalibrierung: Für KIRSCHE und APFEL (und auf Wunsch SALWEIDE) nimmt der Job als Schwelle den Median der GTS an den Tagen der ersten eigenen Meldung dieser Phase in bis zu drei Vorjahren. Ohne Vorjahresmeldung gibt es keine Schwelle; dann gilt der Kalender. So erfindet ApiCore keine Schwellen.

Saisonabschnitte:

Abschnitt ab Phase bis vor Phase
WINTERRUHE WINTER oder vor SALWEIDE SALWEIDE
FRUEHJAHR SALWEIDE Beginn Schwarmzeit
SCHWARMZEIT KIRSCHE (einstellbar: APFEL) LINDE (einstellbar)
SOMMER LINDE TRACHTENDE
SPAETSOMMER TRACHTENDE HERBST
HERBST HERBST WINTER

Beginn der Schwarmzeit ab Kirschblüte ist eine Annahme nach Bieneninstitut-Praxis. Früher prüfen kostet eine Durchsicht, ein verpasster Schwarm kostet die Ernte eines Volks.

Config PHENOLOGY: { useGts: true, gtsThresholds: { SALWEIDE: 200 }, calibrateFromReports: true, calendarFallback: { SALWEIDE: '03-20', … }, swarmSeason: { from: 'KIRSCHE', until: 'LINDE' }, maxGapShare: 0.1 }.

Stände ohne Koordinaten: nur Meldungen und Kalender. Die Begründung sagt „Ohne Koordinaten keine Schätzung“.

7.4 Biotechnik-Ketten

Auslöser Schritt (Vorschlag, Art) fällig ab Auslöser-Datum Quelle
Durchsicht mit queenCellsPresent = true „Schwarmkontrolle“ (PLAN) +7 Tage Fahrplan §10; Bieneninstitute (Verdeckelung nach 8–9 Tagen)
QueenAssigned „Annahme der Königin prüfen“ (PLAN) +7 Tage Fahrplan §10
Ableger RAISE_OWN „Ableger-Plan übernehmen“ (PLAN, drei Termine) +9 „Nachschaffungszellen prüfen“; +21 „Begattung prüfen“ (Fenster bis +24); +28 „Stifte prüfen“ Fahrplan §10
Ableger QUEEN_CELL „Stifte prüfen“ (PLAN) +21 Tage Annahme
Ableger QUEEN „Annahme der Königin prüfen“ (PLAN) +7 Tage wie Königin zugesetzt
Ableger ohne Angabe keine Kette – keine stille Annahme
  • Ein Ableger-Plan ist ein Vorschlag. „Übernehmen“ legt drei Aufgaben mit Terminen an. Eine Entscheidung, drei Kalendereinträge.
  • Dubletten: Ein offener Vorschlag oder eine offene Aufgabe mit derselben Absicht für dasselbe Volk verhindert einen zweiten. Ein Ableger mit QUEEN plus spätere Zuweisung der Königin ergibt also nur einen Annahme-Vorschlag.
  • Fälligkeit zählt ab dem fachlichen Datum (Durchsicht, Zuweisung, Ableger), nicht ab heute.

7.5 Wartezeit-Kette

  • Ein Helfer withdrawal-window.ts: Fenster einer Behandlung = (endDate ?? date) bis (endDate ?? date) + withdrawalPeriodDays. Nutzer: getWithdrawalStatus, withdrawalEndDay (Rückgrat), WithdrawalCheckService, Kalender.
  • WithdrawalCheckService.conflictsForHarvests(db, harvestIds): je Ernte und Volk die Behandlung, deren Fenster das Erntedatum einschließt (Annahme 10). Ohne eingetragene Wartezeit keine Warnung (Annahme 10).
  • Ernte, Schleuderung, Abfüllung: Die Antwort trägt warnings mit Code WITHDRAWAL_ACTIVE (Volk, Mittel, „bis“). Nie 4xx.
  • Meldung: ein Handler je Stufe. Schlüssel withdrawal-conflict:<harvestId>:<colonyId>:<userId>. Jeder Konflikt wird nur einmal gemeldet, egal auf welcher Stufe er zuerst auffällt.
  • „Volk erntefähig“: WithdrawalEnded → INFO „{Volk} ist wieder erntefähig. Wartezeit nach {Mittel} endete am {Datum}.“ an Inhaber von harvests:create. Nur, wenn für das Volk kein weiteres Fenster läuft.
  • Kalender: Quelle WITHDRAWAL je Behandlung mit Wartezeit, ganztägig vom Fensterbeginn bis zum Ende, Titel „Wartezeit {Volk} · {Mittel}“. In-App-Kalender: Typ WITHDRAWAL mit endDate. Ohne Wartezeit wird die Quelle abgemeldet (Graph-Event gelöscht, Rückgrat Task 8).

7.6 Honig-Kette: Vorbelegung und Plausibilität

Vorbelegung ist kein Messwert. Was eine Messung ist (Menge, Wassergehalt), erscheint als Hinweis mit Übernahme-Knopf, nicht still im Feld. Dafür wird PreviousValueHint (T3) um ein Präfix erweitert: „Aus 2 Ernten: 42,5 kg · übernehmen“.

Formular Vorschlag Herkunft
Schleuderung Menge = Σ netWeight der gewählten Ernten GET /extractions/prefill
Schleuderung Sorte, wenn alle Ernten dieselbe honeyType haben dto
Honigcharge Menge = Schleudermenge − Σ initialAmount vorhandener Chargen der Schleuderung GET /honey-batches/prefill
Honigcharge Sorte, Wassergehalt der Schleuderung dto
Prüfung Code Stufe
Schleudermenge > Σ Netto der Ernten (alle Netto bekannt) EXTRACTION_EXCEEDS_HARVEST WARNUNG
Netto fehlt bei mindestens einer Ernte HARVEST_NET_MISSING HINWEIS
Σ Ausgangsmenge der Chargen > Schleudermenge BATCHES_EXCEED_EXTRACTION WARNUNG

Geprüft wird beim Anlegen und Ändern. Antwort 201/200 mit warnings, nie eine Sperre.

7.7 Bewegungsbuch der Honigcharge

HoneyBatchLedgerService.bookIn(tx, { honeyBatchId, deltaKg, type, reason?, notes?, sourceBottlingBatchId?, actingUserId }):

  1. SELECT … FROM "HoneyBatch" WHERE id = $1 FOR UPDATE.
  2. Bei negativem Delta: Rest ≥ |Delta| − 0,5 g, sonst 400 „Abfüllmenge übersteigt die verfügbare Menge der Honigcharge“ (Text wie heute).
  3. BatchMovement in Gramm schreiben, amount auf Gramm gerundet setzen, nie unter 0.
Vorgang Buchung
Charge anlegen mit Menge initialAmount = amount, keine Buchung
Abfüllung anlegen BOTTLING, −Menge, mit sourceBottlingBatchId
Abfüllung ändern BOTTLING, Differenz
Abfüllung entfernen BOTTLING, +Menge, Notiz „Abfüllung entfernt“
PATCH /honey-batches/:id mit amount CORRECTION, Differenz neu − aktuell
Entnahme (lose Ware, Probe, Bruch) WITHDRAWAL mit WithdrawalReason
amount: null bei vorhandenen Buchungen 400 „Die Menge wird über das Bewegungsbuch geführt. Bitte eine Korrektur buchen.“
amount von null auf einen Wert setzt initialAmount, keine Buchung

takeFromHoneyBatch, giveBackToHoneyBatch und normalizeAmount in bottling.service.ts entfallen. Ein Wächter verbietet Schreibzugriffe auf honeyBatch.amount außerhalb des Ledgers.

7.8 Qualität vor der Abfüllung

Grenze Standard Quelle Wirkung
Warnung über 18,0 % Wasser D.I.B., Warenzeichen „Echter Deutscher Honig“ WATER_CONTENT_HIGH, WARNUNG
Bestätigung über 20,0 % Wasser HonigV Anlage 2 (RL 2001/110/EG Anhang II): im Allgemeinen höchstens 20 % WATER_CONTENT_ABOVE_LEGAL, Abfüllen nur mit Bestätigung
Heidekrauthonig Bestätigung über 23 % HonigV Anlage 2 nur, wenn die Sorte in der Config so heißt; keine Vorbelegung
kein Messwert – – WATER_CONTENT_MISSING, HINWEIS
  • Maßgeblich ist die jüngste HoneyMeasurement (nach measuredAt), sonst HoneyBatch.waterContent.
  • Sorten-Abgleich: honeyType ohne Groß-/Kleinschreibung, exakt. Config { warnAbovePercent: 18, confirmAbovePercent: 20, byHoneyType: {} }.
  • Dieselbe Prüfung meldet sich auch beim Erfassen einer Honigmessung (nur Warnung).
  • Bestätigung: POST /bottling ohne acknowledgedWarnings: ['WATER_CONTENT_ABOVE_LEGAL'] → 409 { code: 'CONFIRMATION_REQUIRED', warnings }. Der Idempotenz-Interceptor gibt den Schlüssel bei einem Fehler frei; die bestätigte Wiederholung darf denselben Schlüssel nutzen.
  • MHD und Glas: Mit gewähltem Artikel sind jarSize und bestBeforeDate optional. Der Server nimmt product.packagingSize und date + shelfLifeMonths (Monatsende abgeschnitten, z. B. 31.01. + 1 Monat = 28./29.02.). Ein gesendeter Wert gewinnt. Weicht jarSize vom Artikel ab: JAR_SIZE_DIFFERS, HINWEIS. Für das MHD gibt es keinen Standardwert; der Imker trägt die Haltbarkeit am Artikel ein.
  • Das Formular füllt Glasgröße und MHD direkt aus dem Artikel (Stammdaten, keine Beobachtung) und nennt die Herkunft: „aus Artikel: 24 Monate“.

7.9 Durchsicht → Volkskarte

CreateInspectionDto.applyToColony?: ('occupiedFrames' | 'broodStatus' | 'feedStatus')[] (@ArrayUnique, höchstens 3).

Feld der Volkskarte aus der Durchsicht Abbildung
occupiedFrames colonyStrength (besetzte Waben) gleich, 0–40 (4.14). Der Altwert strength wird nie geschrieben.
broodStatus broodPattern (1–5, 5 = gut) 5 EXCELLENT, 4 GOOD, 3 AVERAGE, 2 WEAK, 1 CRITICAL (Brutbild als Brutstatus = Annahme)
feedStatus feedStatus gleich (4.4)
  • Der Server nimmt die Werte aus der Durchsicht selbst, nie aus dem Request. Es gibt keine zweite Wahrheit.
  • In derselben Transaktion: Durchsicht, Audit, InspectionRecorded, dann ColoniesService.updateIn (eigener Audit-Eintrag Colony UPDATE).
  • Übersprungen und gemeldet in colonyTakeover.skipped wird ein Feld bei:
    • NO_VALUE: Feld in der Durchsicht leer.
    • NO_PERMISSION: kein colonies:update. Kein 403, damit die Offline-Warteschlange nicht hängen bleibt.
    • NEWER_INSPECTION: Es gibt eine neuere Durchsicht des Volks (nach date), z. B. bei einem spät gesendeten Offline-Eintrag.
    • OUT_OF_RANGE: Wert passt nicht zur Volkskarte.
  • Antwort: { …inspection, colonyTakeover: { applied: Field[], skipped: { field, reason }[] } }.
  • Beim Bearbeiten einer Durchsicht wird nichts übernommen. Eine Korrektur löst keine Regel aus (wie Rückgrat Task 11).
  • Das Formular zeigt den Abschnitt „Volkskarte“ direkt über „Speichern“: je Feld eine Zeile „Futter: Ausreichend → Niedrig“ mit Haken. Angehakt sind nur Felder, die in dieser Durchsicht ausgefüllt wurden. Ohne colonies:update fehlt der Abschnitt.

7.10 Volk beendet

  1. In der Transaktion des Endes: releaseQueenIn (5.4). Das gilt auch beim Verlust über die Auswinterung und bei update auf SOLD/DECEASED. Der Status der Königin ändert sich hier nicht.
  2. Handler suggestions.colony-ended:
    • offene Vorschläge des Volks → OBSOLETE;
    • gibt es offene Aufgaben des Volks oder eine Königin mit Status ACTIVE, die eben freigegeben wurde: ein Vorschlag CLEANUP (DO), Priorität NORMAL, Begründung „B3 ist eingegangen (Auswinterung 12.03.). 2 Aufgaben sind noch offen.“
  3. „Aufräumen“ öffnet ein Sheet mit Vorauswahl, alles sichtbar und änderbar:
    • Aufgaben: „Abbrechen“ (CANCELLED) vorgewählt; je Aufgabe umschaltbar auf „Erledigt“ oder „Behalten“.
    • Königin: bei DECEASED „verstorben“, bei SOLD „verkauft“ vorgewählt; bei DISSOLVED/MERGED „unverändert“ (Annahme).
    • Ein Hauptknopf „Aufräumen“.

8. Saison-Autopilot

8.1 Eingaben (je lebendem Volk)

  • Volk: Status, Typ, Volkskarte, Stand, createdAt.
  • Letzte Durchsicht: Datum, colonyStrength, feedStatus, queenCellsPresent.
  • Letzte Varroa-Messung und ihr Band (7.1); letzte Behandlung und ihr Fenster.
  • Fütterungen seit Beginn von SPAETSOMMER bzw. seit der letzten Durchsicht.
  • Königin: hatchYear. Überwinterungseinträge (WinterCheck) des laufenden und des letzten Winters.
  • Offene Aufgaben (relatedType = COLONY) und offene Vorschläge des Volks.
  • ApiaryPhenology des Stands: Phase, Abschnitt, Quelle, Datum der Meldung.
  • Heute (Europe/Berlin) und die Config von SEASON_AUTOPILOT, INSPECTION_INTERVAL, PHENOLOGY.

Keine Eingabe aus Wetter-Prognosen, keine Waage, kein ML.

8.2 Regeln

Ausgabe jeder Regel ist ein Kandidat für einen Vorschlag mit source = AUTOPILOT, ruleKey = SEASON_AUTOPILOT (A1: INSPECTION_INTERVAL).

Nr Absicht Wann (alle Bedingungen) Schritt, Art Begründung (Beispiel) Anker Quelle
A1 INSPECT Abschnitt ≠ WINTERRUHE; inspectionDueOn ≤ heute + 2 „Durchsicht“, DO „Letzte Durchsicht vor 8 Tagen. In der Schwarmzeit alle 7 Tage.“ ID der letzten Durchsicht oder none 7.2
A2 VARROA_MEASURE Abschnitt ≠ WINTERRUHE und letzte Messung älter als 28 Tage; oder WINTERRUHE, 15.11.–20.12., keine Messung seit 01.11. „Varroa messen“, DO „Letzte Messung vor 35 Tagen.“ / „Vor der Winterbehandlung den Milbenfall messen.“ Mess-ID bzw. winter-<Jahr> 28 Tage aus dem Rückgrat; Winterfenster = Annahme nach LWG-Konzept (Restentmilbung im brutfreien Volk)
A3 VARROA_RECHECK Behandlungsende vor ≥ 14 Tagen, keine Messung seither „Behandlungserfolg messen“, DO „Behandlung mit Oxuvar endete am 02.08. Nachkontrolle ab 16.08.“ Behandlungs-ID Fahrplan §10 (LWG)
A4 SUMMER_TREATMENT Abschnitt SPAETSOMMER; keine Behandlung seit Abschnittsbeginn „Sommerbehandlung planen“, PLAN „Tracht vorbei (gemeldet am 25.07.). Letzte Ernte am 20.07.“ Jahr LWG-Varroakonzept: Behandlung nach der letzten Ernte
A5 FEED Abschnitt SPAETSOMMER oder HERBST; keine Fütterung seit Beginn SPAETSOMMER „Einfüttern“, DO „Tracht vorbei. Noch keine Fütterung erfasst.“ Jahr Imkerpraxis; bewusst ohne Zielmenge
A6 FEED Abschnitt ≠ WINTERRUHE; Futter der letzten Durchsicht WEAK/CRITICAL oder Volkskarte CRITICAL; keine Fütterung seither „Füttern“, DO „Futter bei der Durchsicht am 03.04.: niedrig.“ Durchsicht-ID Lastenheft §23
A7 HONEY_SUPER Phase ≥ KIRSCHE und < LINDE; colonyStrength der letzten Durchsicht ≥ 8 „Honigraum prüfen“, DO „Kirsche blüht (geschätzt). B3 besetzt 9 Waben.“ Jahr Imkerpraxis; 8 Waben = Annahme (honeySuperMinFrames)
A8 QUEEN_REPLACE Abschnitt SCHWARMZEIT oder SOMMER; hatchYear ≤ Jahr − 2 „Umweiseln planen“, PLAN „Königin Q-12 ist von 2024.“ Königin-ID und Jahr Imkerpraxis; 2 Jahre = Annahme (queenMaxAgeYears)
A9 WINTER_CHECK Abschnitt HERBST; keine Einwinterung für den Winter dieses Jahres „Einwinterung erfassen“, DO „Für den Winter 2026/27 fehlt die Einwinterung.“ <winterYear>-AUTUMN Annahme 16
A10 WINTER_CHECK Abschnitt FRUEHJAHR; Einwinterung des Vorwinters da, Auswinterung fehlt „Auswinterung erfassen“, DO „Salweide blüht. Auswinterung 2025/26 fehlt.“ <winterYear>-SPRING Annahme 16
  • VARROA_TREAT entsteht nur aus der Varroa-Regel (7.1), nicht aus dem Autopilot.
  • Fälligkeit: A1 inspectionDueOn; A2/A3 der Tag, ab dem die Bedingung gilt; sonst heute.
  • Priorität: A1 überfällig HIGH, sonst NORMAL; A4 HIGH; übrige NORMAL.
  • Ablauf: expiresAt = Fälligkeit + 30 Tage (suggestionTtlDays, Annahme für eine kurze Liste).
  • Config SEASON_AUTOPILOT: { honeySuperMinFrames: 8, varroaMeasureIntervalDays: 28, winterVarroaWindow: { from: '11-15', to: '12-20' }, queenMaxAgeYears: 2, suggestionTtlDays: 30, maxStepsPerColony: 3, rules: { A1: true, …, A10: true } }. Jede Regel einzeln abschaltbar.

8.3 Abgleich (AutopilotService.syncColonyIn(tx, colonyId, today))

  1. Volkszeile sperren. Volk beendet → Ende.
  2. Eingaben über tx laden (eine feste Folge von Abfragen, keine N+1).
  3. evaluateAutopilot(input, config, today) ist eine reine Funktion. Sie liefert Kandidaten { intent, mode, reasonCode, evidence, dueDate, priority, anchor }.
  4. Kandidat verwerfen, wenn für das Volk schon eine offene Aufgabe oder ein offener Vorschlag derselben Absicht besteht.
  5. Neue Kandidaten anlegen (skipDuplicates über dedupeKey). Offene Autopilot-Vorschläge mit gleichem Schlüssel bekommen aktuelle Belege, Fälligkeit und Priorität.
  6. Offene Autopilot-Vorschläge des Volks, deren Schlüssel nicht mehr vorkommt → RESOLVED („Bedingung entfällt“). Beispiel: Die Durchsicht ist erfasst, also verschwindet „Durchsicht fällig“.
  7. Regel- und Kettenvorschläge fasst der Abgleich nicht an.
  8. Ist SEASON_AUTOPILOT aus, werden alle offenen Autopilot-Vorschläge RESOLVED („Autopilot aus“).

8.4 Rangfolge (deterministisch)

Schritte eines Volks = offene Aufgaben (OPEN, IN_PROGRESS) + offene Vorschläge, nach Leserechten gefiltert. Sortiert nach diesem Tupel:

  1. Dringlichkeit: IN_PROGRESS oder überfällig oder heute = 0; in ≤ 3 Tagen = 1; in ≤ 14 Tagen = 2; später = 3; ohne Datum = 4. Ohne Datum, aber URGENT = 0.
  2. Priorität: URGENT, HIGH, NORMAL, LOW.
  3. Absicht (feste Tabelle): VARROA_TREAT, QUEEN_CHECK, QUEEN_ACCEPTANCE, SWARM_CHECK, SPLIT_PLAN, FEED, INSPECT, VARROA_RECHECK, HEALTH_CHECK, SUMMER_TREATMENT, VARROA_MEASURE, WINTER_CHECK, HONEY_SUPER, QUEEN_REPLACE, FOLLOW_UP, CLEANUP, STOCK_REORDER. Eine Aufgabe ohne Vorschlag zählt wie FOLLOW_UP.
  4. Fälligkeit aufsteigend, dann createdAt, dann id.

Zusammenlegen: Schritte, die dasselbe Formular öffnen (z. B. INSPECT und SWARM_CHECK → Durchsicht), werden ein Schritt. Er erbt den dringlichsten Rang und zeigt höchstens zwei Gründe: „Durchsicht mit Schwarmkontrolle. Weiselzellen am 05.05. gesehen. In der Schwarmzeit alle 7 Tage.“

8.5 Ausgabe „die nächsten 3 Schritte“

interface NextStep {
  key: string;                         // 'task:<id>' | 'suggestion:<id>'
  kind: 'TASK' | 'SUGGESTION';
  intent: SuggestionIntent | null;
  title: string;                       // „Durchsicht mit Schwarmkontrolle“
  reasons: string[];                   // höchstens 2 kurze Sätze
  dueOn: string | null;                // YYYY-MM-DD
  urgency: 'OVERDUE' | 'TODAY' | 'SOON' | 'LATER' | 'NONE';
  action:
    | { type: 'CAPTURE'; form: 'inspection' | 'varroa' | 'feeding' | 'treatment' | 'winter-check'; href: string }
    | { type: 'ACCEPT'; suggestionId: string }
    | { type: 'CLEANUP'; suggestionId: string }
    | { type: 'OPEN_TASK'; taskId: string; href: string };
  dismissible: boolean;
}
interface ColonyNextSteps {
  colonyId: string; segment: SeasonSegment; phase: PhenologyPhase | null; phaseSource: PhaseSource | null;
  inspectionDueOn: string | null; withdrawalUntil: string | null; steps: NextStep[]; // höchstens 3
}

Beispiel für B3 am 12.05.: (1) „Durchsicht mit Schwarmkontrolle“ – überfällig seit 1 Tag, „Weiselzellen am 05.05. gesehen.“ – Erfassen; (2) „Behandlung prüfen“ – „12,4 Milben pro Tag am 10.05. Grenze: 10 (LWG).“ – Übernehmen; (3) „Honigraum prüfen“ – „Kirsche blüht (gemeldet am 18.04.). B3 besetzt 9 Waben.“ – Erfassen.

withdrawalUntil erscheint als Hinweis „Wartezeit bis 20.08.“, nicht als Schritt.

8.6 Was der Autopilot nicht tut

  • Er legt keine Aufgaben an. Nur „Übernehmen“ tut das.
  • Er ändert keine Fachdaten. Er schreibt nur Vorschläge.
  • Er rät keine Mengen (Futter, Ernte) und keine Diagnosen.
  • Er nutzt keine Prognosen, nur Erfasstes, Gemeldetes und gemessenes Wetter.

9. Vorschläge

9.1 Lebenszyklus

von nach Auslöser
– OPEN Handler oder Job (proposeIn)
OPEN ACCEPTED „Übernehmen“ (PLAN); legt Aufgabe(n) über TasksService.createInTx an
OPEN DISMISSED „Verwerfen“
DISMISSED OPEN „Rückgängig“ oder „Wieder öffnen“ (bis 30 Tage)
OPEN RESOLVED Abgleich: Bedingung entfällt (DO); Aufräumen ausgeführt (CLEANUP)
OPEN EXPIRED Job, expiresAt überschritten
OPEN OBSOLETE Volk beendet
  • Jeder Übergang ist ein Compare-and-Set (UPDATE … WHERE status = 'OPEN'), mit Audit-Eintrag (entityType: 'Suggestion').
  • Ein DO-Vorschlag hat im Menü „Als Aufgabe planen“. Das ist „Übernehmen“ mit Datum.

9.2 Dubletten und Anker

  • Je Regel und Objekt gibt es höchstens eine offene Sache: Besteht schon ein offener Vorschlag oder eine offene Aufgabe, entsteht kein neuer Vorschlag. Das hält Annahme 12. Die Prüfung läuft unter der Zeilensperre der Regel wie in triggerActionIn und zusätzlich über dedupeKey.
  • Ein verworfener Vorschlag kommt für denselben Anker nie wieder. Ein neuer Anker (neue Messung, neue Durchsicht, neues Jahr) ergibt einen neuen Vorschlag.
  • Regel-Vorschläge: Anker = auslösender Datensatz. Zeitbasierte: Anker = Tag oder Jahr.

9.3 Texte

  • src/suggestions/suggestion-texts.ts: je reasonCode eine Vorlage (evidence, locale de-DE) → { title, reasons }. Zahlen und Daten deutsch formatiert.
  • Höchstens zwei Sätze, je Satz höchstens 90 Zeichen. Ein Wächter prüft jede Vorlage mit Beispielbelegen.
  • actionTemplate einer Regel überschreibt nur den Titel, wie heute.

9.4 Meldungen zu Vorschlägen

  • Nur Regeln mit config.notify = true schreiben zusätzlich eine Meldung (WARNUNG, bei URGENT KRITISCH) an Inhaber von tasks:read mit dem Leserecht des Vorschlags.
  • Alle anderen Vorschläge stehen nur in der Liste. Weniger Lärm.

9.5 Bestehende Automatik-Aufgaben

  • Offene Aufgaben mit sourceAutomationTrigger bleiben Aufgaben. Sie gelten als übernommen.
  • Das Abzeichen TaskSourceBadge heißt künftig „Aus Vorschlag“ (statt „Vorschlag“). Echte Vorschläge sind keine Aufgaben mehr.

9a. Erinnerungen und Telegram (Inhaber-Antwort 3)

9a.1 Überblick

daily-evaluation ──TaskDue──▶ Handler notifications.task-due ──┐
daily-evaluation Schritt 5 (digest) ───────────────────────────┤
alle anderen Meldungen (recordForPermission) ──────────────────┤
                                                               ▼
                    Notification (je Nutzer)  +  NotificationDelivery (TELEGRAM, PENDING)
                                                               │  gleiche Transaktion, pg_notify('notification_delivery','')
                                                               ▼
Worker-Schleife telegram-send ── sendMessage ──▶ Telegram Bot API
Worker-Schleife telegram-updates ◀── getUpdates (Long Polling) ── /start <token>, /stop, /hilfe
  • Handler und Jobs schreiben nur Zeilen. Netz gibt es nur in den zwei Worker-Schleifen (Rückgrat-Handler-Vertrag).
  • OutboxTable bekommt 'NotificationDelivery'. Claim, Abschluss, Freigabe und Wiederholung kommen unverändert aus outbox-claims.ts und retry-policy.ts. WAKE_CHANNELS bekommt notificationDeliveries: 'notification_delivery'.
  • Ohne TELEGRAM_BOT_TOKEN laufen beide Schleifen nicht. Meldungen in der App gibt es trotzdem.

9a.2 Meldung je fälliger Aufgabe

  • Auslöser: TaskDue (Rückgrat, einmal am Fälligkeitstag, mit Nachholfenster).
  • Empfänger: der Zuständige (assignedUserId); ohne Zuständigen alle aktiven Nutzer mit tasks:complete. Nur, wer taskDueEnabled hat und tasks:read hält.
  • Text: „Heute fällig: {Titel}“, Link /tasks. Kategorie TASK_DUE, requiredPermissions: ['tasks:read'].
  • Telegram-Zeitpunkt: frühestens eine Minute nach der Morgenzeit des Empfängers, also nach der Übersicht.
  • Erledigte oder abgebrochene Aufgabe vor dem Versand: Der Sender überspringt sie (SKIPPED, „Aufgabe schon erledigt“).

9a.3 Morgenübersicht

  • Job-Schritt 5, je aktivem Nutzer mit digestEnabled. Schlüssel digest:<YYYY-MM-DD>:<userId>, also höchstens eine je Tag.

  • Inhalt, jede Zeile nur mit dem nötigen Recht:

    Zeile Beispiel Recht
    Aufgaben „2 Aufgaben fällig, 1 davon überfällig.“ tasks:read
    Durchsicht „Durchsicht fällig: 4 Völker am Stand Wald.“ colonies:read und inspections:read
    Vorschläge „3 Vorschläge warten.“ (gezählt nach requiredPermission) tasks:read
    Saison „Stand Wald: Schwarmzeit beginnt. Durchsicht alle 7 Tage.“ apiaries:read
  • Erste Zeile „Guten Morgen. Heute für dich:“, Link /dashboard. requiredPermissions = alle Rechte, die eine Zeile genutzt hat.

  • Ist keine Zeile zu füllen, entsteht keine Übersicht (weniger Lärm; Annahme).

  • digestTime liegt zwischen 05:00 und 12:00. Der Job läuft um 04:30, die Übersicht ist also immer vorher fertig. Die Telegram-Zustellung bekommt availableAt = heute, digestTime, Europe/Berlin.

  • Inhalt mit Stand von 04:30. Später angelegte Aufgaben kommen erst am nächsten Tag in die Übersicht; die Meldung je Aufgabe kommt trotzdem.

9a.4 Zustellung anlegen

In derselben Transaktion wie die Notification (recordForUsers/recordForPermission):

  1. Telegram-Zustellung nur, wenn telegramEnabled, ein TelegramLink besteht und die Kategorie in telegramCategories steht. Standard: TASK_DUE, DIGEST; die übrigen schaltet der Nutzer zu.
  2. availableAt: der spätere Wert aus „jetzt“, Kategorie-Zeit (9a.2, 9a.3) und Ende der Ruhezeit, falls „jetzt“ in der Ruhezeit liegt.
  3. pg_notify('notification_delivery', '') nach dem Einfügen (wie Rückgrat).

Systemantworten des Bots (9a.5) umgehen die Ruhezeit, weil sie auf eine Handlung des Nutzers antworten.

9a.5 Verbinden und Trennen

  1. Profil > Benachrichtigungen > „Telegram verbinden“. Erst ein Step-up wie beim Backup-Import: aktuelles Passwort, bei aktiver MFA dazu TOTP. Ein neuer Kanal nach außen ist eine sensible Änderung.
  2. POST /profile/telegram/link-token erzeugt 32 Zufallsbytes, Base64url (43 Zeichen, erlaubt im start-Parameter). Gespeichert wird nur der SHA-256-Hash. Gültig 10 Minuten, nur einmal. Ein neues Token macht ältere offene Token des Nutzers ungültig. Antwort: { deepLink: 'https://t.me/<bot>?start=<token>', expiresAt }. Drossel wie sensible Aktionen (10/min).
  3. Die App zeigt einen großen Knopf „In Telegram öffnen“ und darunter „Gültig bis {Uhrzeit}“.
  4. Telegram sendet /start <token>. Die Schleife telegram-updates verarbeitet jedes Update in einer eigenen Transaktion:
    • Nur private Chats. Gruppen und Kanäle werden ignoriert.
    • Token-Hash suchen; gültig heißt: nicht verbraucht, expiresAt > now() (DB-Uhr). Verbrauchen per Compare-and-Set.
    • TelegramLink anlegen oder ersetzen. War der Chat mit einem anderen Nutzer verbunden, wird diese Verbindung gelöst; jener Nutzer bekommt eine Meldung in der App.
    • telegramEnabled = true.
    • Meldung in der App (SYSTEM): „Telegram verbunden mit „{Vorname}“ am {Datum}. Warst du das nicht? In den Einstellungen trennen.“ Das deckt einen mitgelesenen Link auf.
    • Antwort LINKED: „Verbunden mit ApiCore. Hier kommen deine Erinnerungen. /stop beendet das.“
    • Ungültiges, abgelaufenes oder benutztes Token: Antwort LINK_INVALID: „Der Link ist ungültig oder abgelaufen. Bitte in ApiCore neu verbinden.“ Kein Hinweis auf Konten.
    • updateOffset = update_id + 1 in derselben Transaktion. Jedes Update wirkt genau einmal.
  5. /stop: Verbindung löschen, telegramEnabled = false, Antwort STOPPED: „Abgemeldet. Du bekommst keine Nachrichten mehr.“
  6. /hilfe, /help und jeder andere Text: Antwort HELP: „Dieser Bot schickt Erinnerungen aus ApiCore. Verbinden geht nur in ApiCore unter Profil > Benachrichtigungen. /stop beendet.“ Höchstens 3 Antworten je Chat in 10 Minuten (Zähler im Empfänger; es gibt immer nur einen, siehe 9a.7).
  7. Trennen in der App: POST /profile/telegram/unlink löscht die Verbindung und schickt STOPPED an den Chat.

9a.6 Versand (telegram-send)

  • Neue Zeile in der Loop-Tabelle von WorkerRuntime: Takt 5 s, Weckkanal notification_delivery, Batch 25 per claimBatch (FOR UPDATE SKIP LOCKED), je Batch höchstens eine Zustellung je Nutzer bzw. Chat.
  • Vor jedem Senden neu prüfen, sonst SKIPPED mit Grund:
    • Nutzer aktiv, telegramEnabled, Verbindung besteht, Kategorie noch eingeschaltet;
    • jedes Recht aus requiredPermissions noch vorhanden (PermissionResolverService);
    • nicht veraltet: TASK_DUE und DIGEST nur am selben Tag (Europe/Berlin), alle anderen höchstens 24 h nach dem Anlegen.
  • Text:
    • Mit Details: message der Meldung, Leerzeile, FRONTEND_ORIGIN + link (Standard /dashboard).
    • Ohne Details (telegramDetail = false): „Neue Meldung in ApiCore.“ plus Link.
    • Klartext ohne parse_mode, also keine Formatierungs-Injektion über Aufgabentitel. Steuerzeichen entfernt, höchstens 600 Zeichen, Link-Vorschau aus.
    • Nie Koordinaten, Adressen oder Notizen. Was in der Meldung steht, durfte der Nutzer schon beim Schreiben sehen und darf es beim Senden noch.
  • Grenzen der Bot API (laut Telegram etwa 1 Nachricht je Sekunde und Chat, etwa 30 je Sekunde insgesamt): Der Sender hält höchstens 25 Nachrichten je Sekunde und je Chat mindestens 1,1 s Abstand.
  • Antworten:
    Antwort Wirkung
    200 DELIVERED, deliveredAt
    429 mit retry_after availableAt = now() + retry_after; zählt nicht als Fehlversuch
    403 (Bot blockiert) oder 400 „chat not found“ Verbindung löschen, telegramEnabled = false, SKIPPED; Meldung in der App: „Telegram-Zustellung beendet. Der Bot wurde blockiert oder der Chat gelöscht.“
    401 (Token ungültig) Zustellung bleibt PENDING mit Wartezeit; Status unter „Verwaltung > System“; Meldung an system:manage einmal je Tag
    5xx, Timeout (10 s), Netzfehler Wiederholung nach retry-policy.ts, nach 8 Versuchen FAILED

9a.7 Empfang: Long Polling statt Webhook

Gewählt: Long Polling im Worker (getUpdates, timeout=25, allowed_updates=["message"]).

Long Polling (gewählt) Webhook
Öffentlicher Eingang keiner; nur ausgehend 443, das der Worker schon darf neue öffentliche Route über Cloudflare und Traefik, Prüfung von secret_token, eigene Drossel
Angriffsfläche keine neue ein unauthentifizierter POST-Endpunkt im Internet
NetworkPolicy, Ingress unverändert Änderung an Ingress und Middleware
Kosten eine gehaltene HTTPS-Verbindung, rund 3 500 kurze Anfragen am Tag nur bei Nachrichten
Bedingung genau ein Empfänger je Bot, kein Webhook gesetzt –
  • Genau ein Empfänger: Lease in TelegramPollState, nur mit DB-Uhr: UPDATE … SET leaseOwner = $me, leaseUntil = now() + interval '60 seconds' WHERE id = 'singleton' AND (leaseUntil IS NULL OR leaseUntil < now() OR leaseOwner = $me) RETURNING updateOffset. Jede Runde verlängert. Wer die Lease nicht hat, wartet 30 s.
  • 409 von Telegram („other getUpdates request“ oder „webhook is active“): lastError setzen, 30 s warten, Hinweis unter „Verwaltung > System“.
  • Die Schleife hört auf das Stop-Signal (AbortController bricht den laufenden Abruf ab).
  • Eine Störung des Empfangs macht den Worker nicht „ungesund“; sie steht nur unter „Verwaltung > System“.

9a.8 Einstellungen (Profil > Benachrichtigungen)

  • Schalter „Erinnerung je Aufgabe“, „Morgenübersicht“ mit Uhrzeit (Standard 06:30), „Ruhezeit“ von–bis (Standard 21:00–06:30).
  • Bereich Telegram: Status („Verbunden mit „{Vorname}“ seit {Datum}“ oder „Nicht verbunden“), Hauptknopf „Telegram verbinden“ bzw. „Trennen“, Auswahl der Kategorien, Schalter „Mit Details“.
  • Ohne eingerichteten Bot: „Telegram ist nicht eingerichtet.“, kein Knopf.
  • Alle Felder bearbeitbar, Null-Regel: quietFrom: null und quietUntil: null heißen „keine Ruhezeit“ (nur beide zusammen).

9a.9 Verwaltung > System

Neue Karte „Telegram“ auf der Seite aus Rückgrat Task 25, Daten aus der System-API (Rückgrat Task 24):

  • Bot eingerichtet ja/nein, Bot-Name.
  • Empfang: Inhaber der Lease, Alter des letzten Abrufs, letzter Fehler.
  • Versand: offene Zustellungen, Alter der ältesten (Warnung ab 10 Minuten), FAILED der letzten 24 h, letzte Fehler (Code und Beschreibung, nie Token oder Chat-ID).
  • „Erneut versuchen“ für FAILED (einzeln und alle), Recht system:manage.

9a.10 Konfiguration und Geheimnisse

Variable Ort Pod Zweck
TELEGRAM_BOT_TOKEN eigenes Secret apicore-telegram, optional: true nur Worker Bot-Token; der Inhaber legt den Bot bei BotFather an und das Secret selbst
TELEGRAM_BOT_USERNAME ConfigMap api, Worker Name für den Deep Link
FRONTEND_ORIGIN vorhanden Worker Links in Nachrichten
  • Der API-Pod bekommt das Token nicht. Er braucht es für den Deep Link nicht.
  • Startprüfung, wenn gesetzt: Format ^\d{5,}:[A-Za-z0-9_-]{30,}$, in Produktion kein Platzhalterwert (wie Annahme zum Schlüsselbund). Sonst startet der Worker nicht.
  • Der Bot-Client baut die URL intern. Fehler sind TelegramApiError { method, statusCode, description } ohne URL. Ein Test stellt sicher, dass das Token in keinem Log und keinem lastError landet.
  • Bei BotFather: Gruppen nicht erlauben (/setjoingroups aus), kein Webhook setzen.
  • Datenschutz: Nachrichten laufen über Server von Telegram und sind dort nicht Ende-zu-Ende verschlüsselt. Darum ist die Verbindung freiwillig, je Nutzer, mit Schalter „Mit Details“.

10. API

Alle Routen unter /api/v1, alle mit @RequirePermission (Abdeckung: test/authorization-coverage.e2e-spec.ts). Anlegen und Entscheiden mit @Idempotent().

10.1 Neue Endpunkte

Methode und Pfad Recht Zweck
GET /suggestions?status=&colonyId=&apiaryId= tasks:read Liste mit gerenderten Texten, gefiltert nach requiredPermission; Standard OPEN
GET /suggestions/summary tasks:read Anzahl offen je Priorität (Badge)
POST /suggestions/:id/accept tasks:create PLAN → Aufgabe(n). Body { dueDate?: string | null; assignedUserId?: string | null }. 409 wenn nicht OPEN
POST /suggestions/bulk-accept tasks:create { ids: string[] } (1–50), alles oder nichts
POST /suggestions/:id/dismiss tasks:update { note?: string } (≤ 500)
POST /suggestions/bulk-dismiss tasks:update { ids: string[] } (1–50)
POST /suggestions/:id/reopen tasks:update DISMISSED → OPEN, höchstens 30 Tage nach der Entscheidung
POST /suggestions/:id/apply-cleanup tasks:update; zusätzlich tasks:complete für „Erledigt“, queens:update für die Königin { tasks: { id; outcome: 'CANCEL' | 'DONE' | 'KEEP' }[]; queen?: { id; status: QueenStatus } | null }; nur IDs aus den Belegen
GET /colonies/:id/next-steps colonies:read ColonyNextSteps (8.5)
GET /autopilot/today colonies:read nächster Schritt über alle Völker, fällige Durchsichten je Stand, Anzahl Vorschläge, Abschnitt je Stand; Aufgaben nur mit tasks:read
GET /phenology/state apiaries:read je Stand Phase, Abschnitt, Quelle, GTS, Datenstand
GET /phenology/reports?year= phenology:read eigene Meldungen
POST /phenology/reports phenology:create { phase; date; apiaryId?: string | null; notes? }; Datum nicht in der Zukunft
PATCH /phenology/reports/:id phenology:update Null-Regel: apiaryId: null = alle Stände, notes: null leert; phase/date nicht leerbar
POST /phenology/reports/:id/revoke phenology:update zurückziehen statt löschen
GET /varroa-measurements/thresholds?colonyId=&date= varroa:read oder varroa:create Bänder je Methode und postTreatmentUntil; ersetzt /varroa-measurements/threshold (T3)
GET /extractions/prefill?harvestIds= extractions:create Σ Netto, Sorte, Quellen, Warnungen
GET /honey-batches/prefill?extractionId= honey-batches:create Restmenge, Sorte, Wassergehalt, Warnungen
GET /honey-batches/:id/movements honey-batches:read Bewegungsbuch
POST /honey-batches/:id/movements honey-batches:update { type: 'WITHDRAWAL' | 'CORRECTION'; deltaKg; reason?; notes? }
GET /bottling/prefill?honeyBatchId=&productId=&date= bottling:create Glasgröße, MHD, Wassergehalt, Warnungen
POST /notifications/read-all angemeldet eigene Meldungen gelesen
GET /profile/notification-settings angemeldet (nur eigene; nie API-Schlüssel) Einstellungen aus 9a.8 und Telegram-Status
PATCH /profile/notification-settings angemeldet Schalter, Zeiten (HH:MM, Morgenzeit 05:00–12:00), Kategorien, Details
POST /profile/telegram/link-token angemeldet + Step-up (Passwort, ggf. TOTP), Drossel 10/min Deep Link, 10 min gültig (9a.5)
POST /profile/telegram/unlink angemeldet Verbindung lösen
GET /system/notification-deliveries?status= system:manage Warteschlange, Fehler, Empfangsstatus (9a.9)
POST /system/notification-deliveries/:id/retry, POST /system/notification-deliveries/retry-failed system:manage FAILED → PENDING

10.2 Geänderte Endpunkte

Endpunkt Änderung
POST /inspections applyToColony; Antwort + colonyTakeover (7.9); feedStatus ist ColonyCondition
POST/PATCH /harvests, /extractions, /honey-batches, /bottling Antwort + warnings
POST /bottling acknowledgedWarnings; jarSize/bestBeforeDate optional mit Artikel; 409 CONFIRMATION_REQUIRED
PATCH /honey-batches/:id amount bucht CORRECTION; null bei Buchungen → 400
POST /colonies/:id/split date?, queenSource?
POST /varroa-measurements (und /bulk, T3) sampleBees; Prozent bei Puderzucker/Auswaschung
GET /colonies/:id/varroa-measurements je Messung rating: { light; reasonCode; band }
GET /apiaries/:id/colony-overview (T3) + inspectionDueOn, segment, nextStepTitle; openWarnings/openCritical aus Vorschlägen
GET /apiaries/:id/offline-pack (T3) + nextSteps je Volk (optional; Version bleibt 1)
GET /calendar Typ WITHDRAWAL mit endDate
GET /notifications, POST /notifications/:id/read nur eigene; Antwort + category, link
POST/PATCH /colonies, GET /colonies/:id + occupiedFrames (0–40); strength bleibt als veralteter Altwert (4.14)
POST/PATCH /inspections colonyStrength höchstens 40
PATCH /automation/rules/:id Config je Regel validiert
GET /dashboard/alerts OLD_QUEEN mit queenMaxAgeYears

10.3 Warnungen (ein Format)

interface PlausibilityWarning {
  code: 'WITHDRAWAL_ACTIVE' | 'EXTRACTION_EXCEEDS_HARVEST' | 'HARVEST_NET_MISSING' | 'BATCHES_EXCEED_EXTRACTION'
      | 'WATER_CONTENT_HIGH' | 'WATER_CONTENT_ABOVE_LEGAL' | 'WATER_CONTENT_MISSING' | 'JAR_SIZE_DIFFERS';
  severity: 'HINWEIS' | 'WARNUNG' | 'BESTAETIGEN';
  message: string;                       // deutsch, serverseitig gerendert
  refs: { type: string; id: string }[];  // nur IDs
}

Ein Ort: src/common/warnings/. Prefill-Endpunkte liefern dieselben Warnungen schon vor dem Speichern.

11. Oberfläche

11.1 Grundsätze

  • Ein Hauptknopf je Ansicht. Alles Weitere ist sekundär oder im Menü.
  • Begründungen in höchstens zwei kurzen Sätzen. Zustand nie nur über Farbe: Icon plus Wort.
  • Vorbelegt wird nur, was keine Beobachtung ist (Glasgröße, MHD aus dem Artikel). Messwerte kommen als Hinweis mit „übernehmen“.
  • Vor Next.js-Code node_modules/next/dist/docs/ lesen (ApiCore-Frontend/AGENTS.md).

11.2 Seiten

Seite Änderung Hauptaktion
Start/Heute (/dashboard) „Nächster Schritt“ aus GET /autopilot/today (offline wie bisher aus Aufgaben). Karte „Durchsicht fällig“ je Stand mit Völkerliste. Chips „Vorschläge {n}“ und Saison je Stand („Schwarmzeit · alle 7 Tage“). der eine nächste Schritt
Vorschläge (/suggestions, neu) Liste nach Stand gruppiert (11.3) je Karte „Übernehmen“ oder „Erfassen“
Volk (/colonies/[id]) Karte „Als Nächstes“ oben (3 Schritte). Varroa-Liste mit Jahreszeit-Ampel. Aktion „Ableger bilden“ mit Datum und Herkunft der Königin. erster Schritt
Stand (/apiaries/[id], T3) Kopf mit Saison und Quelle, Knopf „Pflanze blüht“ (Sheet). Zeilen zeigen „Durchsicht fällig in 2 Tagen“ bzw. „seit 3 Tagen“ und den nächsten Schritt. „Runde starten“ (T3)
Durchsicht neu (T3) Abschnitt „Volkskarte“ über „Speichern“ (7.9) „Speichern“
Varroa neu (T3) Ampel aus Bändern; Feld „Probe (Bienen)“ bei Puderzucker/Auswaschung mit Chip „500 Bienen“; Hinweis „Behandlungseffekt“ „Speichern“
Schleuderung, Honigcharge Hinweise „Aus 2 Ernten: 42,5 kg · übernehmen“; Warnliste über „Speichern“ „Speichern“
Honigcharge Detail Karte „Bewegungen“ (Muster wie components/stock/stock-card); „Menge korrigieren“ bucht –
Abfüllung neu Glasgröße und MHD aus dem Artikel mit Herkunft; Bestätigungsdialog bei über 20 % „Abfüllen“
Kalender Wartezeiten als Spanne –
Verwaltung > Automatisierung Formulare für die neuen Regeln und Werte; Quelle je Wert sichtbar „Speichern“
Meldungen (Glocke) „Alle gelesen“; Meldungen mit link öffnen das Ziel –
Volk bearbeiten Feld „Besetzte Waben“ (NumberStepper, 0–40); „Volksstärke (alt, Skala 1–10)“ nur, wenn gespeichert „Speichern“
Profil > Benachrichtigungen (neu) Schalter, Uhrzeiten, Telegram-Bereich (9a.8) „Telegram verbinden“ bzw. „Speichern“
Verwaltung > System Karte „Telegram“ (9a.9) –

Die Karte „Durchsicht fällig“ startet mit „Runde starten“ eine T3-Runde nur mit den fälligen Völkern dieses Stands (startRound mit gefilterter Liste, Reihenfolge nach Code).

11.3 Vorschlagsliste

  • Kopf: „{n} Vorschläge“ und ein Satz: „Übernehmen plant eine Aufgabe. Verwerfen blendet den Vorschlag aus.“
  • Gruppen: je Stand, dann „Betrieb“ (Lager, Rohstoffe). In der Gruppe nach Rang (8.4).
  • Zusammengefasst: Gleiche Absicht im selben Stand wird eine Zeile:
    • INSPECT: „Durchsicht fällig · 6 Völker“ mit „Runde starten“.
    • PLAN-Absichten: „Honigraum prüfen · 3 Völker“ mit „Alle übernehmen“ (bulk-accept). Aufklappen zeigt die einzelnen Völker.
  • Karte (SuggestionCard): Icon der Absicht, Titel, Begründung, Fälligkeit (DueChip), Hauptknopf („Übernehmen“, „Erfassen“ oder „Aufräumen“), daneben „Verwerfen“ als Text-Knopf. Menü: „Als Aufgabe planen“ (DO), „Termin ändern“ (PLAN; kleines Datums-Sheet, dann Übernehmen).
  • Nach dem Übernehmen: Toast „Aufgabe angelegt · Ansehen“.
  • Nach dem Verwerfen: Toast „Verworfen · Rückgängig“ (8 s). Unten der Link „Verworfene (30 Tage)“ mit „Wieder öffnen“. Damit hängt nichts an der Toast-Zeit (WCAG 2.2.1).
  • Leer: „Keine offenen Vorschläge.“
  • Navigation: „Vorschläge“ in nav-config.ts, Gruppe „Arbeit“, mit Zähler, Recht tasks:read.

11.4 Bausteine

  • Wiederverwendet: Sheet, ChoiceGroup, ToggleChip, ConfirmDialog, DueChip, TaskSourceBadge, StatusLightBadge, EmptyState, QueryState, PageHeader, FormSection, StickySubmit, NumberStepper, PreviousValueHint (Präfix-Prop), RecordSelect, EnumSelect, useCaptureMutation, emptyToNull, toApiDate, Runden-Funktionen aus T3.
  • Neu: SuggestionCard, SuggestionGroup, NextStepsCard, SeasonChip, PhenologyReportSheet, WarningList, ColonyTakeoverSection, HoneyBatchMovementsCard, NotificationSettingsCard, TelegramLinkCard (Step-up über den vorhandenen Passwort-/TOTP-Dialog des Backup-Imports).
  • Neue Lib-Module: lib/suggestions, lib/autopilot, lib/phenology, lib/warnings, lib/notification-settings, lib/colonies/condition.ts (FEED_CONDITION_LABELS, ratingToCondition).
  • PhenologyReportSheet: Zeilen mit 56 px (min-h-14), je Phase Name und Pflanze. Vorgewählt ist „für alle Stände“. Ein Tipp meldet für heute. Offline über useCaptureMutation in die Warteschlange.
  • Varroa-Diagramm (T3): Bänder als Stufenlinie je Monat. Beim Bau den dataviz-Skill laden.

11.5 Barrierefreiheit

  • Touch-Ziele mindestens 44 px, Sheet-Zeilen 56 px.
  • Warnungen und Live-Werte mit role="status" und aria-live="polite"; Bestätigung als Dialog mit Fokusfalle und Rückkehr des Fokus.
  • Ampeln immer mit Icon und Wort („Gut“, „Achtung“, „Kritisch“, „Behandlungseffekt“).
  • Zusammengefasste Gruppen sind Buttons mit aria-expanded.
  • Kontrast über die condition-*-Tokens.

12. Sicherheit

  • Rechte: Jede neue Route mit @RequirePermission. Vorschläge und Schritte werden je Eintrag nach requiredPermission gefiltert. Wer kein varroa:read hat, sieht weder „12,4 Milben pro Tag“ noch den Vorschlag. Gleiches gilt für Behandlungen (treatments:read) und Durchsichten (inspections:read).
  • Keine Rechteausweitung: „Übernehmen“ verlangt tasks:create. Aufräumen prüft jedes Teilrecht einzeln. Die Volkskarte ändert sich nur mit colonies:update.
  • Benachrichtigungen nur an Nutzer mit passendem Recht (Fan-out über recordForPermission). Kein Leck über Rundmeldungen mehr. Bewusste Abweichung von Rückgrat-Spec §4, siehe Entscheidungen.
  • Telegram: Rechte beim Schreiben und beim Senden geprüft; Token nur im Worker, eigenes Secret, nie in Logs; Verbinden nur mit Step-up, Einmal-Token als Hash, 10 Minuten; Meldung in der App bei jeder neuen Verbindung; kein öffentlicher Eingang (Long Polling); Klartext ohne parse_mode; Schalter „Mit Details“. Einzelheiten 9a.5–9a.10.
  • Netz: Nur Worker-Schleifen rufen Wetterdienste und Telegram auf. Feste Hosts aus der Konfiguration, numerische Koordinaten, also kein SSRF-Weg. Handler und Jobs bleiben ohne Netz (Wächter). NetworkPolicies unverändert.
  • Standort: Koordinaten gehen auf 0,01° gerundet hinaus (Diebstahlschutz, wie Rückgrat-Spec §5.4). GET /phenology/state gibt keine Koordinaten aus.
  • Eingaben: Config je Regel validiert; Ober- und Untergrenzen; Listen begrenzt (Bulk 50, applyToColony 3). Freitexte (note) mit Längengrenze.
  • Payloads und Logs: Ereignisse und Belege tragen nur IDs, Enums, Zahlen, Daten. Fehler über loggableError().
  • Integrität: Entscheidungen per Compare-and-Set; Ledger per Zeilensperre; Audit-Einträge in derselben Transaktion.
  • Idempotenz: alle neuen POSTs der Warteschlange mit @Idempotent().

13. Fehlerfälle

Fall Verhalten
Worker fällt aus Vorschläge und Schritte kommen verspätet. Die Übernahme in die Volkskarte läuft synchron und ist nicht betroffen. Der Job gleicht täglich alles nach.
Wetterdienst nicht erreichbar Nächster Takt versucht es. Die Phase nutzt die letzte GTS mit „Stand: {Datum}“. Ist sie älter als 3 Tage, gilt der Kalender.
Stand ohne Koordinaten Nur Meldung und Kalender; Hinweis in der Begründung.
Lückenhafte Wetterdaten (> 10 %) GTS nicht benutzt, Hinweis „Wetterdaten lückenhaft“.
Zwei Handler für dasselbe Volk gleichzeitig Zeilensperre auf dem Volk; der zweite sieht den Stand des ersten.
Doppeltes „Übernehmen“ (Doppeltipp, zwei Geräte) Gleicher Idempotency-Key → gespeicherte Antwort. Anderer Key → 409 „Der Vorschlag ist nicht mehr offen.“ mit aktuellem Status. Genau eine Aufgabe.
Übernehmen nach Ende des Volks Vorschlag ist OBSOLETE → 409.
Verworfen, Ursache besteht weiter Kein neuer Vorschlag für denselben Anker. Neuer Anker → neuer Vorschlag.
Ungültige Regel-Config PATCH → 400 mit Feld. Ein von Hand verdorbener Wert in der DB → Standardwert, Log-Warnung.
Offline-Durchsicht mit Volkskarte kommt spät Neuere Durchsicht vorhanden → Feld übersprungen (NEWER_INSPECTION), Durchsicht gespeichert.
Volkskarte ohne colonies:update Durchsicht 201, Felder übersprungen (NO_PERMISSION).
Futter-Freitext in alten Daten oder Backups Abbildung nach 4.4, Rest in notes. Nichts geht verloren.
Zwei Abfüllungen gleichzeitig über den Rest Ledger-Sperre: eine gelingt, die andere 400 mit Resttext.
Mengenkorrektur unter 0 400 „Die Menge kann nicht unter 0 sinken.“
Wassergehalt über Grenze ohne Bestätigung 409 CONFIRMATION_REQUIRED; Formular zeigt Dialog; bestätigte Wiederholung 201. Aus der Warteschlange: Eintrag bleibt mit Servertext, Tipp öffnet den Dialog.
Wartezeit-Konflikt auf drei Stufen Warnung in jeder Antwort, Meldung nur einmal je (Ernte, Volk).
Meldung einer Phase in der Zukunft 400 „Das Datum liegt in der Zukunft.“
Falsch getippte Meldung „Zurückziehen“; Zustand wird neu berechnet; Phase kann zurückgehen.
Phase rückwärts (zurückgezogene Meldung) SeasonPhaseChanged mit Quelle; offene DO-Vorschläge passen sich beim Abgleich an.
Job scheitert in Schritt 3 Schritte 1, 2, 4 und 5 laufen trotzdem. JobRun FAILED mit Zählern; Meldung an automation:update (einmal je Tag). Handler halten die betroffenen Völker aktuell.
Autopilot ausgeschaltet Offene Autopilot-Vorschläge → RESOLVED; Regel- und Kettenvorschläge bleiben.
Alter Backup-Import Abbildungen nach 4.13; Ledger-Invariante danach geprüft. Telegram-Verbindungen sind nicht im Backup; Nutzer verbinden neu.
Volk mit altem strength, ohne occupiedFrames Anzeige „Volksstärke (alt): 7 von 10“; nichts wird umgerechnet. Die nächste Durchsicht mit Übernahme setzt occupiedFrames.
colonyStrength über 40 400 „Höchstens 40 besetzte Waben.“; die Prüfabfrage aus Migration 10 findet Altfälle vor dem Deploy.
Job läuft nicht (CronJob gescheitert) Keine Morgenübersicht und keine Meldung je Aufgabe an diesem Tag. Die Meldung „Job fehlgeschlagen“ kommt in der App und, falls eingeschaltet, per Telegram (Kategorie SYSTEM). Am Folgetag holt das Nachholfenster TaskDue nach; veraltete Übersichten werden nicht nachgeschickt.
Telegram nicht erreichbar Zustellungen bleiben PENDING, Wiederholung nach Rückgrat-Regel, nach 8 Versuchen FAILED. In der App ist alles da.
Telegram meldet 429 Warten laut retry_after, kein Fehlversuch.
Nutzer blockiert den Bot 403 → Verbindung gelöst, Zustellung SKIPPED, Hinweis in der App.
Bot-Token falsch oder widerrufen 401 → Zustellungen warten; „Verwaltung > System“ zeigt „Bot-Token ungültig“; Meldung an system:manage einmal je Tag.
Zwei Worker Versand über SKIP LOCKED ohne Dubletten; Empfang nur beim Inhaber der Lease.
Ein zweiter Prozess pollt denselben Bot (z. B. Testumgebung mit Produktions-Token) 409 von Telegram, lastError sichtbar; Abhilfe: je Umgebung ein eigener Bot.
Deep Link mitgelesen, Fremder verbindet zuerst Token einmalig und 10 Minuten gültig; der Nutzer sieht sofort „Telegram verbunden mit „{Vorname}““ in der App und trennt mit einem Tipp.
Rechte entzogen zwischen Schreiben und Senden Prüfung beim Senden → SKIPPED.
Zustellung veraltet (Worker lange aus) Übersicht und Aufgaben-Meldung nur am selben Tag, Rest höchstens 24 h; danach SKIPPED.
Morgenzeit vor 05:00 gewünscht 400 „Bitte eine Uhrzeit zwischen 05:00 und 12:00 wählen.“

14. Tests

TDD: zuerst der fehlschlagende Test. Testnamen englisch wie im Bestand.

14.1 Unit (Backend)

  • varroa-bands.spec.ts als Tabelle: 10.0/Tag im August → yellow, 10.1 → red, 0.6 im November → red, Jungvolk 5.1 im Juni → red, Puderzucker 5.1 % im Juli → red, Puderzucker im April → no band, measurement 5 days after treatment end → no light, post-treatment.
  • inspection-due.spec.ts: Intervall je Abschnitt, WINTERRUHE → null, ohne Durchsicht ab createdAt, erste Durchsicht im Jahr.
  • gts.spec.ts: Faktoren 0,5/0,75/1, negative Tage zählen 0, Lückenquote, 200 °C.
  • phenology-phase.spec.ts: Maximum aus Meldung/GTS/Kalender, zurückgezogene Meldungen zählen nicht, „alle Stände“ gilt je Stand, Kalibrierung als Median der Vorjahre, ohne Schwelle Kalender.
  • season-segment.spec.ts: Phase → Abschnitt inkl. eingestelltem Schwarmzeit-Beginn.
  • autopilot-rules.spec.ts: je Regel A1–A10 eine Zeile „gegeben → erwartet“, inklusive Anker und Begründungscode.
  • next-step-ranking.spec.ts: Tupel-Reihenfolge, IN_PROGRESS zuerst, Zusammenlegen gleicher Formulare, höchstens 3, stabile Reihenfolge über id.
  • suggestions.service.spec.ts: proposeIn respektiert offene Aufgabe und verworfenen Anker; accept legt Aufgaben über createInTx an; CAS bei zweitem Übernehmen.
  • suggestion-texts.spec.ts: jeder reasonCode rendert, höchstens 2 Sätze, deutsche Zahlen.
  • rule-config.spec.ts: je Trigger Standard bei ungültigen Werten; Ablehnung beim Update.
  • colony-takeover.spec.ts: Abbildung und alle Überspring-Gründe.
  • feed-vocabulary.spec.ts: Tabelle aus 4.4.
  • honey-batch-ledger.spec.ts: Buchen, zu wenig Rest, Grammrundung, Korrektur als Differenz, Null-Regeln.
  • water-content.spec.ts: jüngste Messung gewinnt, Sorte, Bestätigungsstufe.
  • withdrawal-window.spec.ts: ein Helfer; getWithdrawalStatus und withdrawalEndDay liefern dasselbe Fenster.
  • notifications.service.spec.ts: record ohne userId kompiliert nicht; recordForPermission vergibt den Schlüssel je Empfänger; legt eine Telegram-Zustellung nur bei eingeschalteter Kategorie und bestehender Verbindung an; availableAt nach Ruhezeit und Kategorie-Zeit.
  • colony-lifecycle.spec.ts: releaseQueenIn beendet Historie und currentQueenId; update auf SOLD und DECEASED ruft ihn auf.
  • occupied-frames.spec.ts: 0–40, strength bleibt beim Schreiben von occupiedFrames unberührt.
  • task-due.handler.spec.ts: Zuständiger bevorzugt; sonst tasks:complete; nichts bei taskDueEnabled = false; Schlüssel je Aufgabe, Tag und Nutzer.
  • digest.spec.ts: Zeilen nur mit Recht; keine Übersicht ohne Inhalt; requiredPermissions vollständig; eine je Tag.
  • telegram-link.spec.ts: Token 43 Zeichen Base64url, nur Hash gespeichert, abgelaufen/benutzt → LINK_INVALID, neues Token entwertet alte, Chat wechselt den Nutzer mit Hinweis.
  • telegram-updates.spec.ts: /start, /stop, /hilfe, Gruppen ignoriert, Antwortgrenze je Chat, Offset in derselben Transaktion, Lease nur mit DB-Uhr.
  • telegram-send.spec.ts: Rechte und Verbindung beim Senden geprüft; Klartext ohne parse_mode, höchstens 600 Zeichen; 429 → retry_after ohne Fehlversuch; 403 → Verbindung gelöst; 401 → wartet; veraltet → SKIPPED.
  • telegram-api-error.spec.ts: Token erscheint weder in TelegramApiError noch in loggableError().
  • weather-source.spec.ts: Auswahl über WEATHER_HISTORY_SOURCE; nur https:-URLs; API-Schlüssel nie in Fehlertexten.

14.2 e2e (Test-DB, mit drainDomainEvents)

  • suggestions.e2e-spec.ts:
    • an inspection without queen and eggs leaves one OPEN suggestion and no task
    • accept creates exactly one task with sourceSuggestionId and a calendar delivery
    • a second accept with another key answers 409
    • a dismissed suggestion is not proposed again for the same inspection
    • a later inspection with the same finding proposes again
  • varroa-seasonal.e2e-spec.ts: 12 mites/day in August proposes a treatment check; 12 mites/day five days after treatment end proposes nothing; thresholds for a young colony are halved.
  • chains.e2e-spec.ts: Weiselzellen → +7; RAISE_OWN → Übernehmen ergibt 3 Aufgaben mit +9/+21/+28; Zuweisung → +7; Ableger QUEEN plus Zuweisung → ein Vorschlag.
  • colony-ended.e2e-spec.ts: Auswinterungsverlust mit markColonyDeceased beendet die Königin-Zuordnung in derselben Transaktion; offene Vorschläge OBSOLETE; Aufräumen bricht 2 Aufgaben ab und erledigt 1; 403 ohne tasks:complete.
  • autopilot.e2e-spec.ts: Schwarmzeit per Meldung, letzte Durchsicht vor 9 Tagen → Schritt 1 INSPECT überfällig; nach neuer Durchsicht RESOLVED; nie mehr als 3 Schritte; ohne varroa:read keine Varroa-Gründe.
  • phenology.e2e-spec.ts: Meldung „alle Stände“ → jeder Stand SALWEIDE, SeasonPhaseChanged einmal je Stand; Zurückziehen rechnet neu; Wiederholung mit gleichem Key ergibt eine Meldung; Zukunftsdatum 400; 403 ohne Recht.
  • daily-evaluation.e2e-spec.ts (erweitert): zweimal laufen ergibt gleiche Vorschlagszahlen; Ablauf nach expiresAt; InspectionOverdue nach Abschnitts-Intervall.
  • weather-history.e2e-spec.ts (Fetch gemockt): fehlende Tage einmal geholt, gerundete Koordinaten, ungültige Werte null, Fehler ohne Zeilen.
  • withdrawal-chain.e2e-spec.ts: Ernte im Fenster → 201 + Warnung + eine Meldung; Schleuderung und Abfüllung → Warnung, keine zweite Meldung; WithdrawalEnded → eine INFO; Kalender-Spanne in der Outbox.
  • honey-chain.e2e-spec.ts: Vorbelegung summiert Netto; beide Plausibilitätswarnungen; Ledger-Invariante nach Anlegen, Abfüllen, Korrigieren, Entfernen; zwei parallele Abfüllungen über den Rest → genau eine 400.
  • honey-ledger-migration.e2e-spec.ts: Altdaten mit Abfüllungen → Invariante hält, Buchungen tragen sourceBottlingBatchId.
  • bottling-quality.e2e-spec.ts: 19 % → 201 mit Warnung; 20,5 % → 409; bestätigt → 201; MHD aus shelfLifeMonths; abweichendes Glas → Hinweis.
  • inspection-takeover.e2e-spec.ts: Futter und Brut übernommen, Audit Colony UPDATE; neuere Durchsicht → übersprungen; ohne colonies:update → 201 und übersprungen.
  • notifications.e2e-spec.ts: Lesen je Nutzer getrennt; Nutzer ohne colonies:read bekommt keine Volksmeldung; Migration verteilt Rundmeldungen.
  • colony-strength.e2e-spec.ts: Altwert strength bleibt nach Migration und PATCH von occupiedFrames erhalten; Übernahme aus der Durchsicht schreibt nur occupiedFrames; 41 → 400.
  • reminders.e2e-spec.ts: TaskDue nach Drain → genau eine Meldung je Empfänger, auch nach erneuter Zustellung; Job zweimal → eine Übersicht je Nutzer; Übersicht ohne Varroa-/Durchsichtszeile für einen Nutzer ohne die Rechte.
  • telegram.e2e-spec.ts (Bot API gemockt, kein echter Netzverkehr):
    • link token -> /start links the chat once; a second /start with the same token answers LINK_INVALID
    • link-token requires step-up and is throttled
    • a due task yields one delivery after the morning time and none inside quiet hours
    • two send loops in parallel deliver each message exactly once
    • only the lease holder polls; a second worker waits
    • a revoked permission skips the delivery at send time
    • GET /system/notification-deliveries shows FAILED entries and retry moves them back to PENDING
  • authorization-coverage.e2e-spec.ts deckt alle neuen Routen ab (Profil-Routen nie für API-Schlüssel).

14.3 Wächter

  • Handler-IDs unter WorkerModule = unter AppModule (inkl. aller T4-Handler).
  • honey-batch-amount-writes.spec.ts: amount der Honigcharge wird nur im Ledger geschrieben.
  • no-network-outside-loops.spec.ts: Wetter-Hosts, api.telegram.org und fetch( nur in src/weather/ bzw. src/telegram/; nie in *.handlers.ts oder src/jobs/. handler-contract.spec.ts verbietet zusätzlich Importe von telegram-client.
  • colony-end-releases-queen.spec.ts: jede Stelle, die einen beendeten Status setzt, ruft releaseQueenIn auf.
  • queen-age-threshold.spec.ts: keine zweite Altersschwelle im Code (OLD_QUEEN_AGE_YEARS existiert nicht mehr; Dashboard und Job lesen queenMaxAgeYears).
  • no-broadcast-notifications.spec.ts: kein notification.create/createMany ohne userId.
  • Startprüfung: TELEGRAM_BOT_TOKEN mit falschem Format oder Platzhalter → Worker startet in Produktion nicht.
  • Jeder AutomationTriggerType hat DEFAULT_RULES, Config-Parser und, falls vorschlagend, Textvorlagen.
  • Jeder SuggestionIntent hat Textvorlage (Backend) sowie Icon und Label (Frontend, Vitest).
  • Backup-Wächter für alle neuen Modelle (4.13); update-dto-null-handling.spec.ts für alle neuen und geänderten DTOs.
  • after-commit-usage.spec.ts mit der verkürzten Altpfad-Liste.

14.4 Frontend (Vitest/RTL)

  • Vorschlagsliste: Gruppierung, „Alle übernehmen“, Verwerfen mit Rückgängig, „Verworfene“, Leerzustand, Namen der Knöpfe.
  • NextStepsCard: höchstens 3, Begründung, Aktionen führen mit colonyId ins richtige Formular.
  • Heute: Quelle des nächsten Schritts, Karte „Durchsicht fällig“ startet eine Runde nur mit fälligen Völkern.
  • PhenologyReportSheet: ein Tipp meldet, 56-px-Zeilen, „für alle Stände“ vorgewählt, offline in der Warteschlange.
  • ColonyTakeoverSection: nur ausgefüllte Felder angehakt, „bisher → neu“, ohne Recht unsichtbar.
  • Varroa-Ampel mit Bändern und Hinweis „Behandlungseffekt“; Abfüll-Bestätigung; MHD aus dem Artikel; Bewegungsbuch-Karte; Kalender-Spanne.
  • Volk bearbeiten: „Besetzte Waben“ 0–40; Altfeld nur mit Wert; Leeren sendet null.
  • NotificationSettingsCard: Uhrzeiten validiert, Ruhezeit nur paarweise leerbar, Speichern sendet nur Geändertes.
  • TelegramLinkCard: Step-up-Dialog vor dem Link, Knopf „In Telegram öffnen“ mit Ablaufzeit, Status „Verbunden mit …“, „Trennen“, Hinweis ohne eingerichteten Bot.
  • Glocke: Meldung mit link öffnet das Ziel.

15. Rollout

  1. Reihenfolge: Rückgrat-Tasks 3–18, 24 und 25 und die T3-Tasks 2, 3, 9, 11, 14 sind gemergt. Dann T4 in drei Lieferungen (Abschnitt 16).
  2. Migrationen in der Reihenfolge aus 4.12. Vor dem Deploy ein Backup ziehen (App-Backup und pg_dump). Nach dem Deploy die Prüfabfragen aus 4.12 ausführen.
  3. Regeln: ensureDefaults legt die neuen Regeln an. Bestehende Configs bleiben; VARROA_THRESHOLD liest den alten Wert weiter. TREATMENT_FOLLOW_UP/FEEDING_FOLLOW_UP bleiben, wie sie sind.
  4. Bestehende Automatik-Aufgaben bleiben Aufgaben (9.5).
  5. Benachrichtigungen: Nach Migration 8 sieht jeder Nutzer seine Kopien. Ungelesene Rundmeldungen bleiben ungelesen.
  6. Worker: neue Schleifen phenology-weather, telegram-send und telegram-updates. Der erste Wetter-Takt holt die Tage seit dem 1. Januar nach (je gerundetem Punkt eine Archiv- und eine Forecast-Anfrage). NetworkPolicy und CronJobs unverändert; daily-evaluation braucht etwas länger (Limit 1800 s reicht).
  7. Volksstärke: Migration 10 ändert keine Daten. Vor dem Deploy die Prüfabfrage auf colonyStrength > 40 laufen lassen; Treffer mit dem Inhaber klären, nicht kürzen.
  8. Wetterquelle: WEATHER_HISTORY_SOURCE=open-meteo in der ConfigMap (Inhaber-Entscheidung, freie API). Ein späterer Wechsel ist eine Änderung der ConfigMap (bezahltes Open-Meteo) bzw. ein neuer Adapter plus ConfigMap (DWD).
  9. Telegram (Lieferung T4c), jeder Schritt mit Freigabe des Inhabers:
    1. Der Inhaber legt bei BotFather einen Bot an (Gruppen aus, kein Webhook), je Umgebung einen eigenen.
    2. Der Inhaber legt das Secret apicore-telegram mit TELEGRAM_BOT_TOKEN an. Die Komponente backend-roles bindet es nur in den Worker ein (optional: true); TELEGRAM_BOT_USERNAME kommt in die ConfigMap.
    3. Deploy; „Verwaltung > System“ muss „Bot eingerichtet“ und einen frischen Abruf zeigen.
    4. Der Inhaber verbindet sein Konto und prüft eine Test-Erinnerung (Aufgabe mit Fälligkeit heute).
    • Ohne Secret bleibt Telegram aus; alles andere läuft.
  10. Webhook-Payload: inspection.created trägt feedStatus künftig als GOOD/WEAK/CRITICAL. Der Shop nutzt dieses Ereignis nicht; im Changelog nennen.
  11. Frontend: T3-Funktionen varroaLight(rate, threshold) und INSPECTION_STALE_DAYS werden ersetzt, nicht parallel gehalten.
  12. Doku:
    • Bestehende Annahmen ändern: 12 (Automatische Aufgaben → Vorschläge) und die T3-Annahme „Ampeln“ (Varroa-Bänder, Fälligkeit).
    • Neue Annahmen jeweils unter der nächsten freien Nummer zum Zeitpunkt des Baus, keine fest vergebenen Nummern: Vorschläge und Anker; Saison aus Pflanzenentwicklung und Wetterquelle (Inhaber-Entscheidung Open-Meteo); Volksstärke in besetzten Waben mit Altwert; Bewegungsbuch der Honigcharge; Benachrichtigungen je Nutzer (Abweichung von Rückgrat-Spec §4); Erinnerungen und Telegram; Wassergehalt; Alter der Königin. Hintergrund: ARCHITECTURE hat 22 und 23 schon belegt, der T3-Plan plant 22–27.
    • ARCHITECTURE §7 (Secret apicore-telegram, nur Worker), §8 (Backup-Ausnahmen) und §10 (T4-Handler, Job-Schritte, Worker-Schleifen) ergänzen. TASKS.md um T4.
  13. Cluster: Jeder Schritt nur mit Freigabe des Inhabers, wie im Rückgrat-Runbook.
  14. Erste Woche: Der Autopilot läuft scharf, ist aber in „Verwaltung > Automatisierung“ mit einem Schalter abschaltbar. Die Kalender-Rückfälle und die 8-Waben-Regel nach der ersten Saison mit den eigenen Meldungen prüfen.

16. Aufwand

Baustein PT
Vorschläge: Modell, Service, Regeln umstellen, Texte, API, Rechte-Filter 2,0
Ketten: Weiselzellen, Königin zugesetzt, Ableger, Volk beendet, Ereignisse queens/colonies 1,5
Varroa-Ampel nach Jahreszeit (Bänder, Endpunkt, Probe, UI) 1,0
Fälligkeit, Autopilot-Kern (Abgleich, Rangfolge, „Als Nächstes“, Heute, Vorschlagsliste) 2,0
Durchsicht → Volkskarte, Futter-Vokabular, Migration 1,0
Wartezeit-Kette: Prüfdienst, Warnungen, Meldungen, Kalender-Spanne 1,0
Benachrichtigungen je Nutzer 0,5
Pflanzenentwicklung: Wetter-Schleife, GTS, Meldungen, Kalibrierung, UI 2,0
Autopilot-Saisonregeln A4–A10 1,0
Honig-Kette: Vorbelegung, Plausibilität, Ereignisse 1,0
Bewegungsbuch der Honigcharge inkl. Rückrechnung 1,5
Qualität vor der Abfüllung 0,5
Volksstärke in besetzten Waben: Feld, Migration, API, Oberfläche 0,25
Wetter-Adapter mit Konfiguration (WeatherHistorySource) 0,25
Erinnerungen in der App: Handler notifications.task-due, Job-Schritt digest, NotificationPreference mit API und Einstellungen-Karte 1,0
Telegram-Versand: Outbox NotificationDelivery, Schleife telegram-send, Rendern, Rechteprüfung beim Senden, Ruhezeit, API-Grenzen, Fehlerbehandlung 1,0
Telegram-Verbinden: Einmal-Token, Step-up, Empfang per Long Polling mit Lease, /start, /stop, /hilfe 1,0
Telegram-Oberfläche (TelegramLinkCard) 0,5
Telegram-Betrieb: Karte unter „Verwaltung > System“, System-API, Secret und Manifeste, Startprüfung 0,5
Doku, Gesamtprüfung 0,5
Summe 20,0

Der Fahrplan nannte etwa 10 PT. Mehr wird es durch Punkte, die §10 nicht einpreist: das Bewegungsbuch der Honigcharge, den Wetterspeicher für die GTS, die Rechte-Filterung von Vorschlägen und Meldungen und den neuen Kanal Telegram (Inhaber-Antwort 3, 4,0 PT inklusive Erinnerungen).

Vorschlag: drei Lieferungen.

  • T4a (≈ 10,25 PT): Vorschläge, Ketten, Varroa-Ampel, Fälligkeit und Autopilot-Kern mit Kalender-Rückfall (ohne GTS), Volkskarte mit besetzten Waben, Wartezeit, Volk beendet, Benachrichtigungen je Nutzer, Erinnerungen in der App. Bringt §30, die Vorschlagsliste und die Morgenübersicht.
  • T4b (≈ 6,75 PT): Pflanzenentwicklung mit GTS, Wetter-Adapter und Meldungen, Saisonregeln A4–A10, Honig-Kette, Bewegungsbuch, Qualität.
  • T4c (≈ 3,0 PT): Telegram. Braucht nur T4a; kann parallel zu T4b laufen.

17. Offene Fragen an den Inhaber

Keine. Die drei Fragen vom 2026-10-13 sind beantwortet und eingearbeitet (Abschnitt 1, „Antworten des Inhabers“):

  • Volksstärke in besetzten Waben, 0–40, Altwerte bleiben (4.14).
  • Freie Open-Meteo-API hinter einem Adapter (Entscheidungen, 6.3).
  • Erinnerungen je Aufgabe und als Morgenübersicht, in der App und per Telegram (9a).

Werte, die der Inhaber nach der ersten Saison prüfen sollte, sind als „Annahme“ markiert und alle einstellbar: Kalender-Rückfälle der Phasen, Beginn der Schwarmzeit, 8-Waben-Regel, Ruhezeit 21:00–06:30, Altersschwelle der Königin.