# 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 ```prisma 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: `::`, z. B. `INSPECTION_INTERVAL:col_1:insp_9`. ### 4.2 Pflanzenentwicklung ```prisma 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 `::`, bei täglichen Meldungen mit Tag, z. B. `withdrawal-ended::` oder `job-failed:::`. - `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/`. 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 `parseConfig(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 | `_t4_enum_values` | `AutomationTriggerType` (9 Werte), `BatchMovementType.BOTTLING`, `CalendarSyncSourceType.WITHDRAWAL` | `migrate diff --exit-code` | | 2 | `_t4_suggestions` | Enums und `Suggestion`, `Task.sourceSuggestionId` | Wächter Backup | | 3 | `_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 | `_t4_phenology` | Enums, drei Modelle, Rechte und Rollen-Zuordnung | Rechte-Abdeckung e2e | | 5 | `_t4_split_queen_source` | `SplitQueenSource`, `ColonyLineage.queenSource` | – | | 6 | `_t4_honey_ledger` | Spalten, CHECK neu, Rückrechnung (siehe unten) | Invarianten-Abfrage liefert 0 Zeilen | | 7 | `_t4_quality_fields` | `VarroaMeasurement.sampleBees`, `Product.shelfLifeMonths` | Null-Regel-Spec | | 8 | `_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 | `_t4_calendar_withdrawal` | `syncWithdrawal` | – | | 10 | `_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 | `_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) ```prisma 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:::`; 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`. 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:::`. 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-` | 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.“ | `-AUTUMN` | Annahme 16 | | A10 | `WINTER_CHECK` | Abschnitt `FRUEHJAHR`; Einwinterung des Vorwinters da, Auswinterung fehlt | „Auswinterung erfassen“, DO | „Salweide blüht. Auswinterung 2025/26 fehlt.“ | `-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“ ```ts interface NextStep { key: string; // 'task:' | 'suggestion:' 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 , /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::`, 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/?start=', 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 `. 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) ```ts 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.