Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed: - webhooks with delivery table, retries and stable delivery id - calendar handlers; a finished task removes its Outlook event - tasks, automations, inspections, varroa, treatments, feedings, harvests, colonies, wintering, stock and bottling record domain events in the same transaction; legacy post-commit calls removed - row locks through one helper (lockRowForUpdate, lockRowsForShare, lockRowsForUpdate) - job framework with JobRun, cli exit codes, cleanup, task templates, daily evaluation, hourly app-backup and the rekey job - system API and page "Verwaltung > System" with retry - @nestjs/schedule removed, guard tests against in-process schedulers Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed: - inspection photo offline without metadata - bulk capture for treatments, feedings and varroa (backend + frontend) - offline stand mode with service worker, offline pages and packs - offline session, cold start and logout on shared devices Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md. Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema drift; frontend lint, typecheck, 913 tests, build. Not rolled out. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
114 KiB
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 Jobdaily-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 aufTasksService.createInTx,AutomationService.…In(tx, …),JobRunService,dailyDedupeKeyund 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
- 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).
- 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).
- 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,/stopund/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). ArtPLANoderDO. - 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 insrc/common/dates.ts). Der Job läuft um 04:30 Berliner Zeit; der UTC-Tag vondailyDedupeKeyist dann derselbe Kalendertag.
4. Datenmodell
Alle Namen ladungstragend. Datumswerte wie bisher als DateTime (Mitternacht UTC), wie Task.dueDate.
4.1 Vorschläge
enum SuggestionStatus { OPEN ACCEPTED DISMISSED RESOLVED EXPIRED OBSOLETE }
enum SuggestionMode { PLAN DO }
enum SuggestionSource { RULE CHAIN AUTOPILOT }
enum SuggestionIntent {
INSPECT SWARM_CHECK QUEEN_CHECK QUEEN_ACCEPTANCE QUEEN_REPLACE SPLIT_PLAN
VARROA_MEASURE VARROA_RECHECK VARROA_TREAT SUMMER_TREATMENT
FEED HONEY_SUPER WINTER_CHECK HEALTH_CHECK FOLLOW_UP STOCK_REORDER CLEANUP
}
model Suggestion {
id String @id @default(cuid())
ruleKey AutomationTriggerType
source SuggestionSource
mode SuggestionMode
intent SuggestionIntent
status SuggestionStatus @default(OPEN)
priority TaskPriority @default(NORMAL)
relatedType TaskRelatedType
relatedId String?
colonyId String?
colony Colony? @relation(fields: [colonyId], references: [id])
reasonCode String
evidence Json // nur IDs, Zahlen, YYYY-MM-DD
requiredPermission String? // z. B. 'varroa:read'; fehlt das Recht, ist der Vorschlag unsichtbar
dueDate DateTime?
expiresAt DateTime?
plan Json? // PLAN mit mehreren Terminen: [{ step, offsetDays, windowDays? }]
dedupeKey String? @unique
sourceEventId String?
decidedAt DateTime?
decidedByUserId String?
decidedByUser User? @relation("SuggestionDecider", fields: [decidedByUserId], references: [id])
decisionNote String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tasks Task[]
@@index([status, colonyId])
@@index([status, dueDate])
@@index([ruleKey, relatedType, relatedId, status])
}
TaskbekommtsourceSuggestionId String?(Relation, Index).sourceAutomationTriggerbleibt und wird beim Übernehmen gesetzt.dedupeKeyist nullbar und@unique, wieDomainEvent.dedupeKey. Kein partieller Index (Rückgrat-Spec §3.5).- Format:
<ruleKey>:<relatedId>:<anker>, z. B.INSPECTION_INTERVAL:col_1:insp_9.
4.2 Pflanzenentwicklung
enum PhenologyPhase { SALWEIDE KIRSCHE APFEL HOLUNDER LINDE TRACHTENDE HERBST WINTER }
enum SeasonSegment { WINTERRUHE FRUEHJAHR SCHWARMZEIT SOMMER SPAETSOMMER HERBST }
enum PhaseSource { REPORT GTS CALENDAR }
model PhenologyReport { // eigene Meldung per Antippen
id String @id @default(cuid())
apiaryId String? // null = alle Stände
apiary Apiary? @relation(fields: [apiaryId], references: [id])
phase PhenologyPhase
date DateTime
notes String?
userId String
user User @relation(fields: [userId], references: [id])
revokedAt DateTime? // zurückgezogen statt gelöscht
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([date])
@@index([apiaryId, phase])
}
model ApiaryPhenology { // abgeleiteter Zustand, schreibt nur der Job/Handler
apiaryId String
year Int
gts Float?
gtsThroughDay DateTime?
missingDays Int @default(0)
estimatedPhase PhenologyPhase?
reportedPhase PhenologyPhase?
effectivePhase PhenologyPhase?
phaseSource PhaseSource?
segment SeasonSegment
segmentSince DateTime
updatedAt DateTime @updatedAt
@@id([apiaryId, year])
}
model ApiaryWeatherDay { // Tagesmittel je Stand, schreibt nur die Worker-Schleife
apiaryId String
day DateTime // Kalendertag Europe/Berlin
meanTempC Float?
source String // 'forecast' | 'archive'
fetchedAt DateTime @default(now())
@@id([apiaryId, day])
}
4.3 Ableger
enum SplitQueenSource { RAISE_OWN QUEEN_CELL QUEEN }(Nachschaffung, reife Weiselzelle, Königin zugesetzt).ColonyLineage.queenSource SplitQueenSource?.nullheißt „unbekannt“. Dann gibt es keine Kette, nichts wird angenommen.SplitColonyDtobekommtdate?(heute: festnew Date()) undqueenSource?.
4.4 Futter-Vokabular
-
Inspection.feedStatus String?→ColonyCondition?. Volkskarte und Durchsicht sprechen dieselbe Sprache. -
Abbildung (ein Helfer
feedConditionFromLegacyinsrc/common/feed-vocabulary.ts, auch für den Backup-Import alter Bundles):alter Wert (getrimmt, ohne Groß-/Kleinschreibung) neu ausreichendGOODniedrigWEAKkritischCRITICALleer nulljeder andere Text null, Text wandert an das Ende vonnotesals „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_SUGARundWASHING_METHODwirdcalculatedValue=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) undBatchMovement.sourceBottlingBatchId String?(FK,onDelete: SetNull, Rückverweis auf die verursachende Abfüllung).BatchMovementType+BOTTLING.quantityist 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. IstinitialAmountnull, wird nicht gebucht (Menge nicht erfasst, wie heute).
4.7 Produkt
Product.shelfLifeMonths Int?(Haltbarkeit in Monaten, kein Standardwert).packagingSizegilt als Gramm wieBottlingBatch.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.userIdwird Pflicht. Rundmeldungen (userId = null) gibt es nicht mehr.Notification.dedupeKey String? @unique. Format<art>:<objekt…>:<userId>, bei täglichen Meldungen mit Tag, z. B.withdrawal-ended:<treatmentId>:<userId>oderjob-failed:<job>:<YYYY-MM-DD>:<userId>.Notification.category NotificationCategory(neu):TASK_DUE,DIGEST,SUGGESTION,WITHDRAWAL,SEASON,SYSTEM. Altzeilen bekommenSUGGESTION(Automatik) bzw.SYSTEM(Job-Fehler).Notification.link String?: relativer App-Pfad (beginnt mit/, kein//, kein Schema), z. B./colonies/<id>. Wird beim Schreiben geprüft.Notification.requiredPermissions String[] @default([]): alle Leserechte, die der Inhalt voraussetzt (die Morgenübersicht nutzt mehrere). Der Telegram-Versand prüft sie beim Senden erneut (9a.6).NotificationsService.recordverlangtuserId. NeurecordForUsers(userIds, input, tx);recordForPermissionbleibt 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).GraphCalendarEventInputbekommt optionalendDate(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-checksan Nicht-System-Rollen (Annahme 16):readan Rollen mitcolonies:read,create/updatean Rollen mitinspections: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_THRESHOLDbehält seinen Namen, die Config wird zum Bänder-Format (7.1). Ein alter WertthresholdMitesPerDaywird alsnaturalFall.redAbovegelesen.- Jede Regel bekommt einen Parser
parse<Trigger>Config(json)insrc/automation/rule-config/. Ungültige Werte fallen auf den Standard zurück, wievarroaThresholdOf.PATCH /automation/rules/:idlehnt ungültige bekannte Schlüssel mit 400 ab. - Bestehende vorschlagende Regeln bekommen
config.notify(Standard:truefürQUEENLESS_NO_EGGS,VARROA_THRESHOLD,COLONY_HEALTH_CRITICAL; sonstfalse).
4.12 Migrationen
Zeitstempel vergibt der Bau. Die Reihenfolge ist bindend. Enum-Werte kommen in eigene Migrationen vor jeder Datenänderung, die sie benutzt: Postgres kann einen neuen Enum-Wert erst nach dem Commit verwenden.
| Nr | Name | Inhalt | Prüfung danach |
|---|---|---|---|
| 1 | <ts>_t4_enum_values |
AutomationTriggerType (9 Werte), BatchMovementType.BOTTLING, CalendarSyncSourceType.WITHDRAWAL |
migrate diff --exit-code |
| 2 | <ts>_t4_suggestions |
Enums und Suggestion, Task.sourceSuggestionId |
Wächter Backup |
| 3 | <ts>_t4_feed_vocabulary |
neue Spalte, Abbildung nach 4.4, Freitext nach notes, alte Spalte weg, Umbenennen |
SQL: Zahl umgezogener Freitexte im Migrationslog; keine Zeile mit Text und ohne Notiz |
| 4 | <ts>_t4_phenology |
Enums, drei Modelle, Rechte und Rollen-Zuordnung | Rechte-Abdeckung e2e |
| 5 | <ts>_t4_split_queen_source |
SplitQueenSource, ColonyLineage.queenSource |
– |
| 6 | <ts>_t4_honey_ledger |
Spalten, CHECK neu, Rückrechnung (siehe unten) | Invarianten-Abfrage liefert 0 Zeilen |
| 7 | <ts>_t4_quality_fields |
VarroaMeasurement.sampleBees, Product.shelfLifeMonths |
Null-Regel-Spec |
| 8 | <ts>_t4_notification_fanout |
Rundmeldungen je aktivem Nutzer kopieren (mit gleichem read), Originale löschen, userId NOT NULL, dedupeKey |
keine Zeile mit userId IS NULL |
| 9 | <ts>_t4_calendar_withdrawal |
syncWithdrawal |
– |
| 10 | <ts>_t4_occupied_frames |
Colony.occupiedFrames Int? mit CHECK 0–40; keine Datenänderung an strength |
Prüfabfrage: keine Inspection.colonyStrength über 40 (sonst vor dem Deploy klären, nicht kürzen) |
| 11 | <ts>_t4_notification_channels |
NotificationCategory, Notification.category/link/requiredPermissions, NotificationPreference, TelegramLink, TelegramLinkToken, NotificationDelivery, TelegramPollState |
Wächter Backup; migrate diff --exit-code |
Migration 8 läuft vor 11. Migration 11 belegt category der Altzeilen nach der Regel aus 4.8 und setzt danach NOT NULL.
Rückrechnung in Migration 6, je Charge mit amount IS NOT NULL:
initialAmount = amount + Σ totalAmountaller Abfüllungen der Charge.- Je Abfüllung eine Buchung
BOTTLINGmit−round(totalAmount × 1000)g,sourceBottlingBatchId,createdAtder 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 wieInspection.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:
strengthheiß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 nimmtstrengthweiter an, damit nichts bricht; Swagger markiert es als veraltet. - Anzeige auf der Volksseite: „{n} besetzte Waben“. Fehlt der neue Wert, aber der alte ist da: „Volksstärke (alt): 7 von 10“.
- Historie bleibt lesbar: Audit-Einträge und Exporte behalten die alten Werte mit ihrem alten Feldnamen. CSV-Export/-Import und Backup führen beide Spalten.
- Der Autopilot (A7) und die Statistik „Entwicklung Volksstärke“ nutzen die Durchsichten (
colonyStrength), nicht die Volkskarte.
4.15 Benachrichtigungskanäle (Inhaber-Antwort 3)
enum NotificationCategory { TASK_DUE DIGEST SUGGESTION WITHDRAWAL SEASON SYSTEM }
enum NotificationChannel { TELEGRAM }
enum NotificationDeliveryStatus { PENDING PROCESSING DELIVERED SKIPPED FAILED }
model NotificationPreference { // je Nutzer, Standard beim ersten Lesen angelegt
userId String @id
user User @relation(fields: [userId], references: [id])
taskDueEnabled Boolean @default(true) // Meldung je fälliger Aufgabe
digestEnabled Boolean @default(true) // Morgenübersicht
digestTime String @default("06:30") // HH:MM, Europe/Berlin, 05:00–12:00
quietFrom String? @default("21:00") // Ruhezeit für Telegram; null = keine
quietUntil String? @default("06:30")
telegramEnabled Boolean @default(false)
telegramCategories NotificationCategory[] @default([TASK_DUE, DIGEST])
telegramDetail Boolean @default(true) // false: nur „Neue Meldung in ApiCore“ plus Link
updatedAt DateTime @updatedAt
}
model TelegramLink {
userId String @id
user User @relation(fields: [userId], references: [id])
chatId BigInt @unique // privater Chat; ein Chat gehört zu genau einem Nutzer
telegramFirstName String? // nur zur Anzeige „Verbunden mit …“
linkedAt DateTime @default(now())
}
model TelegramLinkToken {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
tokenHash String @unique // SHA-256 des Tokens, nie der Klartext
expiresAt DateTime // 10 Minuten
consumedAt DateTime?
createdAt DateTime @default(now())
@@index([userId])
}
model NotificationDelivery { // Outbox wie WebhookDelivery
id String @id @default(cuid())
channel NotificationChannel
userId String? // gesetzt bei Meldungen; Chat wird beim Senden aus TelegramLink gelesen
user User? @relation(fields: [userId], references: [id])
chatId BigInt? // nur für Systemantworten an einen Chat (auch unverbundene)
notificationId String?
notification Notification? @relation(fields: [notificationId], references: [id], onDelete: Cascade)
templateCode String? // Systemantworten des Bots (Verbunden, Ungültig, Abgemeldet, Hilfe)
status NotificationDeliveryStatus @default(PENDING)
attempts Int @default(0)
availableAt DateTime @default(now())
claimedAt DateTime?
lastStatusCode Int?
lastError String?
deliveredAt DateTime?
createdAt DateTime @default(now())
@@index([status, availableAt])
@@index([userId, createdAt])
}
model TelegramPollState { // eine Zeile: Offset und Lease des Empfängers
id String @id @default("singleton")
updateOffset BigInt @default(0)
leaseOwner String?
leaseUntil DateTime?
lastPollAt DateTime?
lastError String?
}
- Der Text einer Zustellung wird nicht gespeichert. Der Versand rendert ihn aus der
Notificationbzw. austemplateCode(9a.6). - CHECK-Constraint: genau eins von (
notificationIdmituserId) oder (templateCodemitchatId). - Ruhezeit und Morgenzeit sind Annahmen (Standard 21:00–06:30 bzw. 06:30) und je Nutzer änderbar.
chatIdist 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.SeasonPhaseChangedhatdedupeKey = dailyDedupeKey('SeasonPhaseChanged', apiaryId, Tag).- Payloads tragen nur IDs, Enum-Werte und Daten. Keine Namen, keine Notizen.
5.2 Handler
| Ereignis | Erzeuger | Handler | Wirkung |
|---|---|---|---|
InspectionRecorded |
inspections | automation.inspection-rules (Rückgrat, geändert) |
QUEENLESS_NO_EGGS, FEED_STATUS_CRITICAL (jetzt feedStatus = CRITICAL) → Vorschlag statt Aufgabe |
InspectionRecorded |
inspections | chains.queen-cells |
queenCellsPresent = true → PLAN „Schwarmkontrolle“, fällig +7 |
InspectionRecorded, InspectionCorrected |
inspections | autopilot.colony-sync |
Schritte des Volks neu abgleichen |
VarroaMeasured, VarroaCorrected |
varroa | automation.varroa-threshold (geändert) |
Band rot → PLAN „Behandlung prüfen“; nicht bis 14 Tage nach Behandlungsende |
VarroaMeasured, VarroaCorrected |
varroa | autopilot.colony-sync |
– |
TreatmentRecorded, TreatmentCorrected |
treatments | automation.treatment-follow-up (Rückgrat), calendar-sync.withdrawal (neu), autopilot.colony-sync |
Wartefenster in die Kalender-Outbox |
FeedingRecorded, FeedingCorrected |
feedings | automation.feeding-follow-up (Rückgrat), autopilot.colony-sync |
– |
HarvestRecorded, HarvestCorrected |
harvests | withdrawal.harvest-check, autopilot.colony-sync (je Volk) |
WARNUNG je Konflikt (Volk, Ernte), dedupliziert |
ExtractionRecorded, ExtractionCorrected |
extractions | withdrawal.extraction-check |
Meldung nur für noch nicht gemeldete Konflikte |
HoneyBatchRecorded |
honey-batches | webhooks.forward |
Webhook batch.created (bisher Altpfad) |
BatchBottled, BottlingCorrected |
bottling | withdrawal.bottling-check |
wie oben |
ColonyStatusChanged |
colonies | automation.colony-health (Rückgrat, geändert), autopilot.colony-sync |
KRITISCH → Vorschlag |
ColonyEnded |
colonies, winter-checks | suggestions.colony-ended |
offene Vorschläge → OBSOLETE; Aufräum-Vorschlag (7.10) |
ColonySplit |
colonies | chains.split-schedule, autopilot.colony-sync (beide Völker) |
PLAN mit Terminen je queenSource |
QueenAssigned |
queens | chains.queen-acceptance, calendar-sync.queen-assignment, webhooks.forward, autopilot.colony-sync |
PLAN „Annahme prüfen“ +7; Kalender und Webhook queen.replaced statt Altpfad |
TaskCompleted, TaskUpdated |
tasks | autopilot.colony-sync (nur relatedType = COLONY) |
– |
WithdrawalEnded |
daily-evaluation | notifications.withdrawal-ended, autopilot.colony-sync |
INFO „B3 ist wieder erntefähig“ an harvests:create, nur wenn kein anderes Fenster mehr läuft |
QueenAged, InspectionOverdue, VarroaMeasurementDue |
daily-evaluation | autopilot.colony-sync |
– |
TaskDue |
daily-evaluation | notifications.task-due |
HINWEIS „Heute fällig: {Titel}“ mit Link an den Zuständigen, ohne Zuständigen an Inhaber von tasks:complete; nur bei taskDueEnabled; Schlüssel task-due:<taskId>:<Tag>:<userId>; Telegram-Zustellung nach 9a.4 |
PhenologyReported, PhenologyCorrected |
phenology | phenology.recompute |
Zustand des Stands bzw. aller Stände neu; schreibt bei Wechsel SeasonPhaseChanged |
SeasonPhaseChanged |
daily-evaluation, phenology | autopilot.apiary-sync, notifications.season-segment |
alle Völker des Stands; INFO nur bei Wechsel des Abschnitts |
5.3 Regeln für alle T4-Handler
- Handler-IDs folgen
HANDLER_ID_PATTERN. Alle stehen in Modulen, dieWorkerModuleimportiert (Carry-forward Rückgrat Task 6). - Nur Schreiben über
tx, keine Netzwerkaufrufe (Wächterhandler-contract.spec.ts). autopilot.colony-syncsperrt 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 })mitdedupeKey. 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 vonupdate. Ihn nutzt auch die Durchsicht (7.9). -
releaseQueenIn(tx, colonyId, endDate)insrc/colonies/colony-lifecycle.ts: offeneQueenColonyHistorymitendDatebeenden,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 updateaufSOLDoderDECEASEDKö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
statusauf einen Wert ausTERMINAL_COLONY_STATUSESsetzt, ruftreleaseQueenInin derselben Transaktion auf. -
ColoniesService.splitschreibtColonySplit, Audit in der Transaktion. -
QueensService.assignColonyschreibtQueenAssigned;webhooks.dispatch('queen.replaced')undcalendarSync.enqueue('QUEEN_ASSIGNMENT')entfallen. -
ExtractionsServiceundHoneyBatchesService: 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 verliertqueensundhoney-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.
- Fehlende Tage der letzten 90 Tage: Forecast-API mit
-
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. -
cleanuplöschtApiaryWeatherDayälter als 4 Kalenderjahre (für die Kalibrierung, 7.3). -
Quelle (Inhaber-Entscheidung): freie Open-Meteo-API. Laut Anbieter ist sie nur für nicht-kommerzielle Nutzung frei; der Inhaber trägt dieses Risiko bewusst. Unser Bedarf liegt weit unter der Tagesgrenze des Anbieters (eine Anfrage je gerundetem Punkt und Tag).
-
Adapter: Die Schleife kennt nur
WeatherHistorySource.dailyMeans(point, fromDay, toDay): Promise<DailyMean[]>. Welche Umsetzung läuft, bestimmen Umgebungsvariablen aus der ConfigMap:Variable Standard Zweck WEATHER_HISTORY_SOURCEopen-meteoAuswahl der Umsetzung OPEN_METEO_FORECAST_URLhttps://api.open-meteo.com/v1/forecastBasis-URL; für das bezahlte Angebot die Kunden-URL OPEN_METEO_ARCHIVE_URLhttps://archive-api.open-meteo.com/v1/archivedto OPEN_METEO_API_KEYleer 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 oderlastError(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ßtWert ≥ 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 ?? dateeiner 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ätHIGH, 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:
inspectionDueOninColonyOverviewRow(T3), inGET /colonies/:id/next-stepsund inGET /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:
- Gemeldet: höchste Phase aller nicht zurückgezogenen Meldungen dieses Jahres für den Stand oder für alle Stände.
- Geschätzt (GTS): höchste Phase mit Schwelle ≤ heutiger GTS, nur wenn die GTS nicht lückenhaft ist.
- 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
QUEENplus 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
warningsmit CodeWITHDRAWAL_ACTIVE(Volk, Mittel, „bis“). Nie 4xx. - Meldung: ein Handler je Stufe. Schlüssel
withdrawal-conflict:<harvestId>:<colonyId>:<userId>. Jeder Konflikt wird nur einmal gemeldet, egal auf welcher Stufe er zuerst auffällt. - „Volk erntefähig“:
WithdrawalEnded→ INFO „{Volk} ist wieder erntefähig. Wartezeit nach {Mittel} endete am {Datum}.“ an Inhaber vonharvests:create. Nur, wenn für das Volk kein weiteres Fenster läuft. - Kalender: Quelle
WITHDRAWALje Behandlung mit Wartezeit, ganztägig vom Fensterbeginn bis zum Ende, Titel „Wartezeit {Volk} · {Mittel}“. In-App-Kalender: TypWITHDRAWALmitendDate. 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 }):
SELECT … FROM "HoneyBatch" WHERE id = $1 FOR UPDATE.- Bei negativem Delta: Rest ≥ |Delta| − 0,5 g, sonst 400 „Abfüllmenge übersteigt die verfügbare Menge der Honigcharge“ (Text wie heute).
BatchMovementin Gramm schreiben,amountauf 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(nachmeasuredAt), sonstHoneyBatch.waterContent. - Sorten-Abgleich:
honeyTypeohne Groß-/Kleinschreibung, exakt. Config{ warnAbovePercent: 18, confirmAbovePercent: 20, byHoneyType: {} }. - Dieselbe Prüfung meldet sich auch beim Erfassen einer Honigmessung (nur Warnung).
- Bestätigung:
POST /bottlingohneacknowledgedWarnings: ['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
jarSizeundbestBeforeDateoptional. Der Server nimmtproduct.packagingSizeunddate + shelfLifeMonths(Monatsende abgeschnitten, z. B. 31.01. + 1 Monat = 28./29.02.). Ein gesendeter Wert gewinnt. WeichtjarSizevom 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, dannColoniesService.updateIn(eigener Audit-EintragColony UPDATE). - Übersprungen und gemeldet in
colonyTakeover.skippedwird ein Feld bei:NO_VALUE: Feld in der Durchsicht leer.NO_PERMISSION: keincolonies:update. Kein 403, damit die Offline-Warteschlange nicht hängen bleibt.NEWER_INSPECTION: Es gibt eine neuere Durchsicht des Volks (nachdate), 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:updatefehlt der Abschnitt.
7.10 Volk beendet
- In der Transaktion des Endes:
releaseQueenIn(5.4). Das gilt auch beim Verlust über die Auswinterung und beiupdateaufSOLD/DECEASED. Der Status der Königin ändert sich hier nicht. - 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 VorschlagCLEANUP(DO), PrioritätNORMAL, Begründung „B3 ist eingegangen (Auswinterung 12.03.). 2 Aufgaben sind noch offen.“
- offene Vorschläge des Volks →
- „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“, beiSOLD„verkauft“ vorgewählt; beiDISSOLVED/MERGED„unverändert“ (Annahme). - Ein Hauptknopf „Aufräumen“.
- Aufgaben: „Abbrechen“ (
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
SPAETSOMMERbzw. 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. ApiaryPhenologydes Stands: Phase, Abschnitt, Quelle, Datum der Meldung.- Heute (Europe/Berlin) und die Config von
SEASON_AUTOPILOT,INSPECTION_INTERVAL,PHENOLOGY.
Keine Eingabe aus Wetter-Prognosen, keine Waage, kein ML.
8.2 Regeln
Ausgabe jeder Regel ist ein Kandidat für einen Vorschlag mit source = AUTOPILOT, ruleKey = SEASON_AUTOPILOT (A1: INSPECTION_INTERVAL).
| Nr | Absicht | Wann (alle Bedingungen) | Schritt, Art | Begründung (Beispiel) | Anker | Quelle |
|---|---|---|---|---|---|---|
| A1 | INSPECT |
Abschnitt ≠ WINTERRUHE; inspectionDueOn ≤ heute + 2 |
„Durchsicht“, DO | „Letzte Durchsicht vor 8 Tagen. In der Schwarmzeit alle 7 Tage.“ | ID der letzten Durchsicht oder none |
7.2 |
| A2 | VARROA_MEASURE |
Abschnitt ≠ WINTERRUHE und letzte Messung älter als 28 Tage; oder WINTERRUHE, 15.11.–20.12., keine Messung seit 01.11. |
„Varroa messen“, DO | „Letzte Messung vor 35 Tagen.“ / „Vor der Winterbehandlung den Milbenfall messen.“ | Mess-ID bzw. winter-<Jahr> |
28 Tage aus dem Rückgrat; Winterfenster = Annahme nach LWG-Konzept (Restentmilbung im brutfreien Volk) |
| A3 | VARROA_RECHECK |
Behandlungsende vor ≥ 14 Tagen, keine Messung seither | „Behandlungserfolg messen“, DO | „Behandlung mit Oxuvar endete am 02.08. Nachkontrolle ab 16.08.“ | Behandlungs-ID | Fahrplan §10 (LWG) |
| A4 | SUMMER_TREATMENT |
Abschnitt SPAETSOMMER; keine Behandlung seit Abschnittsbeginn |
„Sommerbehandlung planen“, PLAN | „Tracht vorbei (gemeldet am 25.07.). Letzte Ernte am 20.07.“ | Jahr | LWG-Varroakonzept: Behandlung nach der letzten Ernte |
| A5 | FEED |
Abschnitt SPAETSOMMER oder HERBST; keine Fütterung seit Beginn SPAETSOMMER |
„Einfüttern“, DO | „Tracht vorbei. Noch keine Fütterung erfasst.“ | Jahr | Imkerpraxis; bewusst ohne Zielmenge |
| A6 | FEED |
Abschnitt ≠ WINTERRUHE; Futter der letzten Durchsicht WEAK/CRITICAL oder Volkskarte CRITICAL; keine Fütterung seither |
„Füttern“, DO | „Futter bei der Durchsicht am 03.04.: niedrig.“ | Durchsicht-ID | Lastenheft §23 |
| A7 | HONEY_SUPER |
Phase ≥ KIRSCHE und < LINDE; colonyStrength der letzten Durchsicht ≥ 8 |
„Honigraum prüfen“, DO | „Kirsche blüht (geschätzt). B3 besetzt 9 Waben.“ | Jahr | Imkerpraxis; 8 Waben = Annahme (honeySuperMinFrames) |
| A8 | QUEEN_REPLACE |
Abschnitt SCHWARMZEIT oder SOMMER; hatchYear ≤ Jahr − 2 |
„Umweiseln planen“, PLAN | „Königin Q-12 ist von 2024.“ | Königin-ID und Jahr | Imkerpraxis; 2 Jahre = Annahme (queenMaxAgeYears) |
| A9 | WINTER_CHECK |
Abschnitt HERBST; keine Einwinterung für den Winter dieses Jahres |
„Einwinterung erfassen“, DO | „Für den Winter 2026/27 fehlt die Einwinterung.“ | <winterYear>-AUTUMN |
Annahme 16 |
| A10 | WINTER_CHECK |
Abschnitt FRUEHJAHR; Einwinterung des Vorwinters da, Auswinterung fehlt |
„Auswinterung erfassen“, DO | „Salweide blüht. Auswinterung 2025/26 fehlt.“ | <winterYear>-SPRING |
Annahme 16 |
VARROA_TREATentsteht 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, sonstNORMAL; A4HIGH; übrigeNORMAL. - 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))
- Volkszeile sperren. Volk beendet → Ende.
- Eingaben über
txladen (eine feste Folge von Abfragen, keine N+1). evaluateAutopilot(input, config, today)ist eine reine Funktion. Sie liefert Kandidaten{ intent, mode, reasonCode, evidence, dueDate, priority, anchor }.- Kandidat verwerfen, wenn für das Volk schon eine offene Aufgabe oder ein offener Vorschlag derselben Absicht besteht.
- Neue Kandidaten anlegen (
skipDuplicatesüberdedupeKey). Offene Autopilot-Vorschläge mit gleichem Schlüssel bekommen aktuelle Belege, Fälligkeit und Priorität. - 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“. - Regel- und Kettenvorschläge fasst der Abgleich nicht an.
- Ist
SEASON_AUTOPILOTaus, werden alle offenen Autopilot-VorschlägeRESOLVED(„Autopilot aus“).
8.4 Rangfolge (deterministisch)
Schritte eines Volks = offene Aufgaben (OPEN, IN_PROGRESS) + offene Vorschläge, nach Leserechten gefiltert. Sortiert nach diesem Tupel:
- Dringlichkeit:
IN_PROGRESSoder überfällig oder heute = 0; in ≤ 3 Tagen = 1; in ≤ 14 Tagen = 2; später = 3; ohne Datum = 4. Ohne Datum, aberURGENT= 0. - Priorität:
URGENT,HIGH,NORMAL,LOW. - 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 wieFOLLOW_UP. - Fälligkeit aufsteigend, dann
createdAt, dannid.
Zusammenlegen: Schritte, die dasselbe Formular öffnen (z. B. INSPECT und SWARM_CHECK → Durchsicht), werden ein Schritt. Er erbt den dringlichsten Rang und zeigt höchstens zwei Gründe: „Durchsicht mit Schwarmkontrolle. Weiselzellen am 05.05. gesehen. In der Schwarmzeit alle 7 Tage.“
8.5 Ausgabe „die nächsten 3 Schritte“
interface NextStep {
key: string; // 'task:<id>' | 'suggestion:<id>'
kind: 'TASK' | 'SUGGESTION';
intent: SuggestionIntent | null;
title: string; // „Durchsicht mit Schwarmkontrolle“
reasons: string[]; // höchstens 2 kurze Sätze
dueOn: string | null; // YYYY-MM-DD
urgency: 'OVERDUE' | 'TODAY' | 'SOON' | 'LATER' | 'NONE';
action:
| { type: 'CAPTURE'; form: 'inspection' | 'varroa' | 'feeding' | 'treatment' | 'winter-check'; href: string }
| { type: 'ACCEPT'; suggestionId: string }
| { type: 'CLEANUP'; suggestionId: string }
| { type: 'OPEN_TASK'; taskId: string; href: string };
dismissible: boolean;
}
interface ColonyNextSteps {
colonyId: string; segment: SeasonSegment; phase: PhenologyPhase | null; phaseSource: PhaseSource | null;
inspectionDueOn: string | null; withdrawalUntil: string | null; steps: NextStep[]; // höchstens 3
}
Beispiel für B3 am 12.05.: (1) „Durchsicht mit Schwarmkontrolle“ – überfällig seit 1 Tag, „Weiselzellen am 05.05. gesehen.“ – Erfassen; (2) „Behandlung prüfen“ – „12,4 Milben pro Tag am 10.05. Grenze: 10 (LWG).“ – Übernehmen; (3) „Honigraum prüfen“ – „Kirsche blüht (gemeldet am 18.04.). B3 besetzt 9 Waben.“ – Erfassen.
withdrawalUntil erscheint als Hinweis „Wartezeit bis 20.08.“, nicht als Schritt.
8.6 Was der Autopilot nicht tut
- Er legt keine Aufgaben an. Nur „Übernehmen“ tut das.
- Er ändert keine Fachdaten. Er schreibt nur Vorschläge.
- Er rät keine Mengen (Futter, Ernte) und keine Diagnosen.
- Er nutzt keine Prognosen, nur Erfasstes, Gemeldetes und gemessenes Wetter.
9. Vorschläge
9.1 Lebenszyklus
| von | nach | Auslöser |
|---|---|---|
| – | OPEN |
Handler oder Job (proposeIn) |
OPEN |
ACCEPTED |
„Übernehmen“ (PLAN); legt Aufgabe(n) über TasksService.createInTx an |
OPEN |
DISMISSED |
„Verwerfen“ |
DISMISSED |
OPEN |
„Rückgängig“ oder „Wieder öffnen“ (bis 30 Tage) |
OPEN |
RESOLVED |
Abgleich: Bedingung entfällt (DO); Aufräumen ausgeführt (CLEANUP) |
OPEN |
EXPIRED |
Job, expiresAt überschritten |
OPEN |
OBSOLETE |
Volk beendet |
- Jeder Übergang ist ein Compare-and-Set (
UPDATE … WHERE status = 'OPEN'), mit Audit-Eintrag (entityType: 'Suggestion'). - Ein DO-Vorschlag hat im Menü „Als Aufgabe planen“. Das ist „Übernehmen“ mit Datum.
9.2 Dubletten und Anker
- Je Regel und Objekt gibt es höchstens eine offene Sache: Besteht schon ein offener Vorschlag oder eine offene Aufgabe, entsteht kein neuer Vorschlag. Das hält Annahme 12. Die Prüfung läuft unter der Zeilensperre der Regel wie in
triggerActionInund zusätzlich überdedupeKey. - 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: jereasonCodeeine 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.
actionTemplateeiner Regel überschreibt nur den Titel, wie heute.
9.4 Meldungen zu Vorschlägen
- Nur Regeln mit
config.notify = trueschreiben zusätzlich eine Meldung (WARNUNG, beiURGENTKRITISCH) an Inhaber vontasks:readmit dem Leserecht des Vorschlags. - Alle anderen Vorschläge stehen nur in der Liste. Weniger Lärm.
9.5 Bestehende Automatik-Aufgaben
- Offene Aufgaben mit
sourceAutomationTriggerbleiben Aufgaben. Sie gelten als übernommen. - Das Abzeichen
TaskSourceBadgeheißt künftig „Aus Vorschlag“ (statt „Vorschlag“). Echte Vorschläge sind keine Aufgaben mehr.
9a. Erinnerungen und Telegram (Inhaber-Antwort 3)
9a.1 Überblick
daily-evaluation ──TaskDue──▶ Handler notifications.task-due ──┐
daily-evaluation Schritt 5 (digest) ───────────────────────────┤
alle anderen Meldungen (recordForPermission) ──────────────────┤
▼
Notification (je Nutzer) + NotificationDelivery (TELEGRAM, PENDING)
│ gleiche Transaktion, pg_notify('notification_delivery','')
▼
Worker-Schleife telegram-send ── sendMessage ──▶ Telegram Bot API
Worker-Schleife telegram-updates ◀── getUpdates (Long Polling) ── /start <token>, /stop, /hilfe
- Handler und Jobs schreiben nur Zeilen. Netz gibt es nur in den zwei Worker-Schleifen (Rückgrat-Handler-Vertrag).
OutboxTablebekommt'NotificationDelivery'. Claim, Abschluss, Freigabe und Wiederholung kommen unverändert ausoutbox-claims.tsundretry-policy.ts.WAKE_CHANNELSbekommtnotificationDeliveries: 'notification_delivery'.- Ohne
TELEGRAM_BOT_TOKENlaufen 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 mittasks:complete. Nur, wertaskDueEnabledhat undtasks:readhält. - Text: „Heute fällig: {Titel}“, Link
/tasks. KategorieTASK_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üsseldigest:<YYYY-MM-DD>:<userId>, also höchstens eine je Tag. -
Inhalt, jede Zeile nur mit dem nötigen Recht:
Zeile Beispiel Recht Aufgaben „2 Aufgaben fällig, 1 davon überfällig.“ tasks:readDurchsicht „Durchsicht fällig: 4 Völker am Stand Wald.“ colonies:readundinspections:readVorschläge „3 Vorschläge warten.“ (gezählt nach requiredPermission)tasks:readSaison „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).
-
digestTimeliegt zwischen 05:00 und 12:00. Der Job läuft um 04:30, die Übersicht ist also immer vorher fertig. Die Telegram-Zustellung bekommtavailableAt= 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):
- Telegram-Zustellung nur, wenn
telegramEnabled, einTelegramLinkbesteht und die Kategorie intelegramCategoriessteht. Standard:TASK_DUE,DIGEST; die übrigen schaltet der Nutzer zu. availableAt: der spätere Wert aus „jetzt“, Kategorie-Zeit (9a.2, 9a.3) und Ende der Ruhezeit, falls „jetzt“ in der Ruhezeit liegt.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
- 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.
POST /profile/telegram/link-tokenerzeugt 32 Zufallsbytes, Base64url (43 Zeichen, erlaubt imstart-Parameter). Gespeichert wird nur der SHA-256-Hash. Gültig 10 Minuten, nur einmal. Ein neues Token macht ältere offene Token des Nutzers ungültig. Antwort:{ deepLink: 'https://t.me/<bot>?start=<token>', expiresAt }. Drossel wie sensible Aktionen (10/min).- Die App zeigt einen großen Knopf „In Telegram öffnen“ und darunter „Gültig bis {Uhrzeit}“.
- Telegram sendet
/start <token>. Die Schleifetelegram-updatesverarbeitet 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. TelegramLinkanlegen 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 + 1in derselben Transaktion. Jedes Update wirkt genau einmal.
/stop: Verbindung löschen,telegramEnabled = false, AntwortSTOPPED: „Abgemeldet. Du bekommst keine Nachrichten mehr.“/hilfe,/helpund jeder andere Text: AntwortHELP: „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).- Trennen in der App:
POST /profile/telegram/unlinklöscht die Verbindung und schicktSTOPPEDan den Chat.
9a.6 Versand (telegram-send)
- Neue Zeile in der Loop-Tabelle von
WorkerRuntime: Takt 5 s, Weckkanalnotification_delivery, Batch 25 perclaimBatch(FOR UPDATE SKIP LOCKED), je Batch höchstens eine Zustellung je Nutzer bzw. Chat. - Vor jedem Senden neu prüfen, sonst
SKIPPEDmit Grund:- Nutzer aktiv,
telegramEnabled, Verbindung besteht, Kategorie noch eingeschaltet; - jedes Recht aus
requiredPermissionsnoch vorhanden (PermissionResolverService); - nicht veraltet:
TASK_DUEundDIGESTnur am selben Tag (Europe/Berlin), alle anderen höchstens 24 h nach dem Anlegen.
- Nutzer aktiv,
- Text:
- Mit Details:
messageder 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.
- Mit Details:
- 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,deliveredAt429 mit retry_afteravailableAt = now() + retry_after; zählt nicht als Fehlversuch403 (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 PENDINGmit Wartezeit; Status unter „Verwaltung > System“; Meldung ansystem:manageeinmal je Tag5xx, Timeout (10 s), Netzfehler Wiederholung nach retry-policy.ts, nach 8 VersuchenFAILED
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“):
lastErrorsetzen, 30 s warten, Hinweis unter „Verwaltung > System“. - Die Schleife hört auf das Stop-Signal (
AbortControllerbricht 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: nullundquietUntil: nullheiß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),
FAILEDder letzten 24 h, letzte Fehler (Code und Beschreibung, nie Token oder Chat-ID). - „Erneut versuchen“ für
FAILED(einzeln und alle), Rechtsystem: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 keinemlastErrorlandet. - Bei BotFather: Gruppen nicht erlauben (
/setjoingroupsaus), kein Webhook setzen. - Datenschutz: Nachrichten laufen über Server von Telegram und sind dort nicht Ende-zu-Ende verschlüsselt. Darum ist die Verbindung freiwillig, je Nutzer, mit Schalter „Mit Details“.
10. API
Alle Routen unter /api/v1, alle mit @RequirePermission (Abdeckung: test/authorization-coverage.e2e-spec.ts). Anlegen und Entscheiden mit @Idempotent().
10.1 Neue Endpunkte
| Methode und Pfad | Recht | Zweck |
|---|---|---|
GET /suggestions?status=&colonyId=&apiaryId= |
tasks:read |
Liste mit gerenderten Texten, gefiltert nach requiredPermission; Standard OPEN |
GET /suggestions/summary |
tasks:read |
Anzahl offen je Priorität (Badge) |
POST /suggestions/:id/accept |
tasks:create |
PLAN → Aufgabe(n). Body { dueDate?: string | null; assignedUserId?: string | null }. 409 wenn nicht OPEN |
POST /suggestions/bulk-accept |
tasks:create |
{ ids: string[] } (1–50), alles oder nichts |
POST /suggestions/:id/dismiss |
tasks:update |
{ note?: string } (≤ 500) |
POST /suggestions/bulk-dismiss |
tasks:update |
{ ids: string[] } (1–50) |
POST /suggestions/:id/reopen |
tasks:update |
DISMISSED → OPEN, höchstens 30 Tage nach der Entscheidung |
POST /suggestions/:id/apply-cleanup |
tasks:update; zusätzlich tasks:complete für „Erledigt“, queens:update für die Königin |
{ tasks: { id; outcome: 'CANCEL' | 'DONE' | 'KEEP' }[]; queen?: { id; status: QueenStatus } | null }; nur IDs aus den Belegen |
GET /colonies/:id/next-steps |
colonies:read |
ColonyNextSteps (8.5) |
GET /autopilot/today |
colonies:read |
nächster Schritt über alle Völker, fällige Durchsichten je Stand, Anzahl Vorschläge, Abschnitt je Stand; Aufgaben nur mit tasks:read |
GET /phenology/state |
apiaries:read |
je Stand Phase, Abschnitt, Quelle, GTS, Datenstand |
GET /phenology/reports?year= |
phenology:read |
eigene Meldungen |
POST /phenology/reports |
phenology:create |
{ phase; date; apiaryId?: string | null; notes? }; Datum nicht in der Zukunft |
PATCH /phenology/reports/:id |
phenology:update |
Null-Regel: apiaryId: null = alle Stände, notes: null leert; phase/date nicht leerbar |
POST /phenology/reports/:id/revoke |
phenology:update |
zurückziehen statt löschen |
GET /varroa-measurements/thresholds?colonyId=&date= |
varroa:read oder varroa:create |
Bänder je Methode und postTreatmentUntil; ersetzt /varroa-measurements/threshold (T3) |
GET /extractions/prefill?harvestIds= |
extractions:create |
Σ Netto, Sorte, Quellen, Warnungen |
GET /honey-batches/prefill?extractionId= |
honey-batches:create |
Restmenge, Sorte, Wassergehalt, Warnungen |
GET /honey-batches/:id/movements |
honey-batches:read |
Bewegungsbuch |
POST /honey-batches/:id/movements |
honey-batches:update |
{ type: 'WITHDRAWAL' | 'CORRECTION'; deltaKg; reason?; notes? } |
GET /bottling/prefill?honeyBatchId=&productId=&date= |
bottling:create |
Glasgröße, MHD, Wassergehalt, Warnungen |
POST /notifications/read-all |
angemeldet | eigene Meldungen gelesen |
GET /profile/notification-settings |
angemeldet (nur eigene; nie API-Schlüssel) | Einstellungen aus 9a.8 und Telegram-Status |
PATCH /profile/notification-settings |
angemeldet | Schalter, Zeiten (HH:MM, Morgenzeit 05:00–12:00), Kategorien, Details |
POST /profile/telegram/link-token |
angemeldet + Step-up (Passwort, ggf. TOTP), Drossel 10/min | Deep Link, 10 min gültig (9a.5) |
POST /profile/telegram/unlink |
angemeldet | Verbindung lösen |
GET /system/notification-deliveries?status= |
system:manage |
Warteschlange, Fehler, Empfangsstatus (9a.9) |
POST /system/notification-deliveries/:id/retry, POST /system/notification-deliveries/retry-failed |
system:manage |
FAILED → PENDING |
10.2 Geänderte Endpunkte
| Endpunkt | Änderung |
|---|---|
POST /inspections |
applyToColony; Antwort + colonyTakeover (7.9); feedStatus ist ColonyCondition |
POST/PATCH /harvests, /extractions, /honey-batches, /bottling |
Antwort + warnings |
POST /bottling |
acknowledgedWarnings; jarSize/bestBeforeDate optional mit Artikel; 409 CONFIRMATION_REQUIRED |
PATCH /honey-batches/:id |
amount bucht CORRECTION; null bei Buchungen → 400 |
POST /colonies/:id/split |
date?, queenSource? |
POST /varroa-measurements (und /bulk, T3) |
sampleBees; Prozent bei Puderzucker/Auswaschung |
GET /colonies/:id/varroa-measurements |
je Messung rating: { light; reasonCode; band } |
GET /apiaries/:id/colony-overview (T3) |
+ inspectionDueOn, segment, nextStepTitle; openWarnings/openCritical aus Vorschlägen |
GET /apiaries/:id/offline-pack (T3) |
+ nextSteps je Volk (optional; Version bleibt 1) |
GET /calendar |
Typ WITHDRAWAL mit endDate |
GET /notifications, POST /notifications/:id/read |
nur eigene; Antwort + category, link |
POST/PATCH /colonies, GET /colonies/:id |
+ occupiedFrames (0–40); strength bleibt als veralteter Altwert (4.14) |
POST/PATCH /inspections |
colonyStrength höchstens 40 |
PATCH /automation/rules/:id |
Config je Regel validiert |
GET /dashboard/alerts |
OLD_QUEEN mit queenMaxAgeYears |
10.3 Warnungen (ein Format)
interface PlausibilityWarning {
code: 'WITHDRAWAL_ACTIVE' | 'EXTRACTION_EXCEEDS_HARVEST' | 'HARVEST_NET_MISSING' | 'BATCHES_EXCEED_EXTRACTION'
| 'WATER_CONTENT_HIGH' | 'WATER_CONTENT_ABOVE_LEGAL' | 'WATER_CONTENT_MISSING' | 'JAR_SIZE_DIFFERS';
severity: 'HINWEIS' | 'WARNUNG' | 'BESTAETIGEN';
message: string; // deutsch, serverseitig gerendert
refs: { type: string; id: string }[]; // nur IDs
}
Ein Ort: src/common/warnings/. Prefill-Endpunkte liefern dieselben Warnungen schon vor dem Speichern.
11. Oberfläche
11.1 Grundsätze
- Ein Hauptknopf je Ansicht. Alles Weitere ist sekundär oder im Menü.
- Begründungen in höchstens zwei kurzen Sätzen. Zustand nie nur über Farbe: Icon plus Wort.
- Vorbelegt wird nur, was keine Beobachtung ist (Glasgröße, MHD aus dem Artikel). Messwerte kommen als Hinweis mit „übernehmen“.
- Vor Next.js-Code
node_modules/next/dist/docs/lesen (ApiCore-Frontend/AGENTS.md).
11.2 Seiten
| Seite | Änderung | Hauptaktion |
|---|---|---|
Start/Heute (/dashboard) |
„Nächster Schritt“ aus GET /autopilot/today (offline wie bisher aus Aufgaben). Karte „Durchsicht fällig“ je Stand mit Völkerliste. Chips „Vorschläge {n}“ und Saison je Stand („Schwarmzeit · alle 7 Tage“). |
der eine nächste Schritt |
Vorschläge (/suggestions, neu) |
Liste nach Stand gruppiert (11.3) | je Karte „Übernehmen“ oder „Erfassen“ |
Volk (/colonies/[id]) |
Karte „Als Nächstes“ oben (3 Schritte). Varroa-Liste mit Jahreszeit-Ampel. Aktion „Ableger bilden“ mit Datum und Herkunft der Königin. | erster Schritt |
Stand (/apiaries/[id], T3) |
Kopf mit Saison und Quelle, Knopf „Pflanze blüht“ (Sheet). Zeilen zeigen „Durchsicht fällig in 2 Tagen“ bzw. „seit 3 Tagen“ und den nächsten Schritt. | „Runde starten“ (T3) |
| Durchsicht neu (T3) | Abschnitt „Volkskarte“ über „Speichern“ (7.9) | „Speichern“ |
| Varroa neu (T3) | Ampel aus Bändern; Feld „Probe (Bienen)“ bei Puderzucker/Auswaschung mit Chip „500 Bienen“; Hinweis „Behandlungseffekt“ | „Speichern“ |
| Schleuderung, Honigcharge | Hinweise „Aus 2 Ernten: 42,5 kg · übernehmen“; Warnliste über „Speichern“ | „Speichern“ |
| Honigcharge Detail | Karte „Bewegungen“ (Muster wie components/stock/stock-card); „Menge korrigieren“ bucht |
– |
| Abfüllung neu | Glasgröße und MHD aus dem Artikel mit Herkunft; Bestätigungsdialog bei über 20 % | „Abfüllen“ |
| Kalender | Wartezeiten als Spanne | – |
| Verwaltung > Automatisierung | Formulare für die neuen Regeln und Werte; Quelle je Wert sichtbar | „Speichern“ |
| Meldungen (Glocke) | „Alle gelesen“; Meldungen mit link öffnen das Ziel |
– |
| Volk bearbeiten | Feld „Besetzte Waben“ (NumberStepper, 0–40); „Volksstärke (alt, Skala 1–10)“ nur, wenn gespeichert |
„Speichern“ |
| Profil > Benachrichtigungen (neu) | Schalter, Uhrzeiten, Telegram-Bereich (9a.8) | „Telegram verbinden“ bzw. „Speichern“ |
| Verwaltung > System | Karte „Telegram“ (9a.9) | – |
Die Karte „Durchsicht fällig“ startet mit „Runde starten“ eine T3-Runde nur mit den fälligen Völkern dieses Stands (startRound mit gefilterter Liste, Reihenfolge nach Code).
11.3 Vorschlagsliste
- Kopf: „{n} Vorschläge“ und ein Satz: „Übernehmen plant eine Aufgabe. Verwerfen blendet den Vorschlag aus.“
- Gruppen: je Stand, dann „Betrieb“ (Lager, Rohstoffe). In der Gruppe nach Rang (8.4).
- Zusammengefasst: Gleiche Absicht im selben Stand wird eine Zeile:
INSPECT: „Durchsicht fällig · 6 Völker“ mit „Runde starten“.- PLAN-Absichten: „Honigraum prüfen · 3 Völker“ mit „Alle übernehmen“ (
bulk-accept). Aufklappen zeigt die einzelnen Völker.
- Karte (
SuggestionCard): Icon der Absicht, Titel, Begründung, Fälligkeit (DueChip), Hauptknopf („Übernehmen“, „Erfassen“ oder „Aufräumen“), daneben „Verwerfen“ als Text-Knopf. Menü: „Als Aufgabe planen“ (DO), „Termin ändern“ (PLAN; kleines Datums-Sheet, dann Übernehmen). - Nach dem Übernehmen: Toast „Aufgabe angelegt · Ansehen“.
- Nach dem Verwerfen: Toast „Verworfen · Rückgängig“ (8 s). Unten der Link „Verworfene (30 Tage)“ mit „Wieder öffnen“. Damit hängt nichts an der Toast-Zeit (WCAG 2.2.1).
- Leer: „Keine offenen Vorschläge.“
- Navigation: „Vorschläge“ in
nav-config.ts, Gruppe „Arbeit“, mit Zähler, Rechttasks: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 überuseCaptureMutationin 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"undaria-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 nachrequiredPermissiongefiltert. Wer keinvarroa:readhat, 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 mitcolonies: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/stategibt keine Koordinaten aus. - Eingaben: Config je Regel validiert; Ober- und Untergrenzen; Listen begrenzt (Bulk 50,
applyToColony3). 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.tsals 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 abcreatedAt, 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_PROGRESSzuerst, Zusammenlegen gleicher Formulare, höchstens 3, stabile Reihenfolge überid.suggestions.service.spec.ts:proposeInrespektiert offene Aufgabe und verworfenen Anker;acceptlegt Aufgaben übercreateInTxan; CAS bei zweitem Übernehmen.suggestion-texts.spec.ts: jederreasonCoderendert, 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;getWithdrawalStatusundwithdrawalEndDayliefern dasselbe Fenster.notifications.service.spec.ts:recordohneuserIdkompiliert nicht;recordForPermissionvergibt den Schlüssel je Empfänger; legt eine Telegram-Zustellung nur bei eingeschalteter Kategorie und bestehender Verbindung an;availableAtnach Ruhezeit und Kategorie-Zeit.colony-lifecycle.spec.ts:releaseQueenInbeendet Historie undcurrentQueenId;updateaufSOLDundDECEASEDruft ihn auf.occupied-frames.spec.ts: 0–40,strengthbleibt beim Schreiben vonoccupiedFramesunberührt.task-due.handler.spec.ts: Zuständiger bevorzugt; sonsttasks:complete; nichts beitaskDueEnabled = false; Schlüssel je Aufgabe, Tag und Nutzer.digest.spec.ts: Zeilen nur mit Recht; keine Übersicht ohne Inhalt;requiredPermissionsvollstä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 ohneparse_mode, höchstens 600 Zeichen; 429 →retry_afterohne Fehlversuch; 403 → Verbindung gelöst; 401 → wartet; veraltet →SKIPPED.telegram-api-error.spec.ts: Token erscheint weder inTelegramApiErrornoch inloggableError().weather-source.spec.ts: Auswahl überWEATHER_HISTORY_SOURCE; nurhttps:-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 taskaccept creates exactly one task with sourceSuggestionId and a calendar deliverya second accept with another key answers 409a dismissed suggestion is not proposed again for the same inspectiona 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; AblegerQUEENplus Zuweisung → ein Vorschlag.colony-ended.e2e-spec.ts: Auswinterungsverlust mitmarkColonyDeceasedbeendet die Königin-Zuordnung in derselben Transaktion; offene VorschlägeOBSOLETE; Aufräumen bricht 2 Aufgaben ab und erledigt 1; 403 ohnetasks:complete.autopilot.e2e-spec.ts: Schwarmzeit per Meldung, letzte Durchsicht vor 9 Tagen → Schritt 1INSPECTüberfällig; nach neuer DurchsichtRESOLVED; nie mehr als 3 Schritte; ohnevarroa:readkeine Varroa-Gründe.phenology.e2e-spec.ts: Meldung „alle Stände“ → jeder StandSALWEIDE,SeasonPhaseChangedeinmal 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 nachexpiresAt;InspectionOverduenach Abschnitts-Intervall.weather-history.e2e-spec.ts(Fetch gemockt): fehlende Tage einmal geholt, gerundete Koordinaten, ungültige Wertenull, 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 tragensourceBottlingBatchId.bottling-quality.e2e-spec.ts: 19 % → 201 mit Warnung; 20,5 % → 409; bestätigt → 201; MHD ausshelfLifeMonths; abweichendes Glas → Hinweis.inspection-takeover.e2e-spec.ts: Futter und Brut übernommen, AuditColony UPDATE; neuere Durchsicht → übersprungen; ohnecolonies:update→ 201 und übersprungen.notifications.e2e-spec.ts: Lesen je Nutzer getrennt; Nutzer ohnecolonies:readbekommt keine Volksmeldung; Migration verteilt Rundmeldungen.colony-strength.e2e-spec.ts: Altwertstrengthbleibt nach Migration und PATCH vonoccupiedFrameserhalten; Übernahme aus der Durchsicht schreibt nuroccupiedFrames; 41 → 400.reminders.e2e-spec.ts:TaskDuenach 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_INVALIDlink-token requires step-up and is throttleda due task yields one delivery after the morning time and none inside quiet hourstwo send loops in parallel deliver each message exactly onceonly the lease holder polls; a second worker waitsa revoked permission skips the delivery at send timeGET /system/notification-deliveries shows FAILED entries and retry moves them back to PENDING
authorization-coverage.e2e-spec.tsdeckt alle neuen Routen ab (Profil-Routen nie für API-Schlüssel).
14.3 Wächter
- Handler-IDs unter
WorkerModule= unterAppModule(inkl. aller T4-Handler). honey-batch-amount-writes.spec.ts:amountder Honigcharge wird nur im Ledger geschrieben.no-network-outside-loops.spec.ts: Wetter-Hosts,api.telegram.orgundfetch(nur insrc/weather/bzw.src/telegram/; nie in*.handlers.tsodersrc/jobs/.handler-contract.spec.tsverbietet zusätzlich Importe vontelegram-client.colony-end-releases-queen.spec.ts: jede Stelle, die einen beendeten Status setzt, ruftreleaseQueenInauf.queen-age-threshold.spec.ts: keine zweite Altersschwelle im Code (OLD_QUEEN_AGE_YEARSexistiert nicht mehr; Dashboard und Job lesenqueenMaxAgeYears).no-broadcast-notifications.spec.ts: keinnotification.create/createManyohneuserId.- Startprüfung:
TELEGRAM_BOT_TOKENmit falschem Format oder Platzhalter → Worker startet in Produktion nicht. - Jeder
AutomationTriggerTypehatDEFAULT_RULES, Config-Parser und, falls vorschlagend, Textvorlagen. - Jeder
SuggestionIntenthat Textvorlage (Backend) sowie Icon und Label (Frontend, Vitest). - Backup-Wächter für alle neuen Modelle (4.13);
update-dto-null-handling.spec.tsfür alle neuen und geänderten DTOs. after-commit-usage.spec.tsmit 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 mitcolonyIdins 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
- 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).
- 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. - Regeln:
ensureDefaultslegt die neuen Regeln an. Bestehende Configs bleiben;VARROA_THRESHOLDliest den alten Wert weiter.TREATMENT_FOLLOW_UP/FEEDING_FOLLOW_UPbleiben, wie sie sind. - Bestehende Automatik-Aufgaben bleiben Aufgaben (9.5).
- Benachrichtigungen: Nach Migration 8 sieht jeder Nutzer seine Kopien. Ungelesene Rundmeldungen bleiben ungelesen.
- Worker: neue Schleifen
phenology-weather,telegram-sendundtelegram-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-evaluationbraucht etwas länger (Limit 1800 s reicht). - Volksstärke: Migration 10 ändert keine Daten. Vor dem Deploy die Prüfabfrage auf
colonyStrength > 40laufen lassen; Treffer mit dem Inhaber klären, nicht kürzen. - Wetterquelle:
WEATHER_HISTORY_SOURCE=open-meteoin 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). - Telegram (Lieferung T4c), jeder Schritt mit Freigabe des Inhabers:
- Der Inhaber legt bei BotFather einen Bot an (Gruppen aus, kein Webhook), je Umgebung einen eigenen.
- Der Inhaber legt das Secret
apicore-telegrammitTELEGRAM_BOT_TOKENan. Die Komponentebackend-rolesbindet es nur in den Worker ein (optional: true);TELEGRAM_BOT_USERNAMEkommt in die ConfigMap. - Deploy; „Verwaltung > System“ muss „Bot eingerichtet“ und einen frischen Abruf zeigen.
- 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.
- Webhook-Payload:
inspection.createdträgtfeedStatuskünftig alsGOOD/WEAK/CRITICAL. Der Shop nutzt dieses Ereignis nicht; im Changelog nennen. - Frontend: T3-Funktionen
varroaLight(rate, threshold)undINSPECTION_STALE_DAYSwerden ersetzt, nicht parallel gehalten. - 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.
- Cluster: Jeder Schritt nur mit Freigabe des Inhabers, wie im Rückgrat-Runbook.
- 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.