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]>
1384 lines
114 KiB
Markdown
1384 lines
114 KiB
Markdown
# T4 Logik-Ketten und Saison-Autopilot — Design
|
||
|
||
Stand: 2026-10-13 · Betrifft: ApiCore (Backend, Frontend) · Teilprojekt T4 aus `docs/superpowers/specs/2026-10-12-plattform-rueckgrat-und-fahrplan-design.md` §10
|
||
|
||
## 1. Ziel
|
||
|
||
Was der Imker erfasst, wirkt weiter. ApiCore zieht daraus die Folgen selbst:
|
||
|
||
- Aus einer Durchsicht, einer Messung oder einem Ableger entstehen datierte Folgeschritte.
|
||
- Jedes Volk hat „die nächsten 3 Schritte“, jeweils mit einer kurzen Begründung.
|
||
- Die Saison kommt aus der Pflanzenentwicklung am Stand, nicht aus einem starren Kalender.
|
||
- Automatik plant nie heimlich. Sie schlägt vor. Der Imker übernimmt oder verwirft mit einem Tipp.
|
||
|
||
Technisch heißt das:
|
||
|
||
- Alle Folgen hängen an Handlern des Ereignis-Rückgrats (`@OnDomainEvent`, `events.record(tx, …)`) oder am Job `daily-evaluation`. Kein T4-Code ruft Folgen direkt nach dem Commit auf.
|
||
- Ein Regelwerk im Code, ohne ML. Jeder fachliche Wert ist je Regel einstellbar und nennt seine Quelle.
|
||
- Ein Muster für alle Vorschläge: ein Modell, ein Service, eine Liste.
|
||
|
||
**Voraussetzungen:**
|
||
- Rückgrat-Plan `docs/superpowers/plans/2026-10-13-plattform-rueckgrat.md`: Tasks 3, 4, 9, 10, 11–15, 17 und 18 sind fertig. T4 baut genau auf `TasksService.createInTx`, `AutomationService.…In(tx, …)`, `JobRunService`, `dailyDedupeKey` und dem Handler-Vertrag auf.
|
||
- T3-Plan `docs/superpowers/plans/2026-10-13-t3-erfassung-am-stand.md`: Tasks 2, 3, 9, 11 und 14 sind fertig. T4 ändert deren Ampeln und ergänzt das Durchsichtsformular. T4 baut nichts davon neu.
|
||
|
||
### Inhaber-Entscheidungen (2026-10-13), bindend
|
||
|
||
- Nur eigener deutscher Honig. Keine Wanderung. Keine Stockwaage. Alles andere aus §10 T4 ist gewollt.
|
||
- Automatische Aufgaben bleiben Vorschläge mit „Übernehmen“ und „Verwerfen“ (Annahme 12).
|
||
- Wartezeit-Prüfungen warnen, sperren nie (Annahme 10).
|
||
- Beobachtungen werden nie still vorbelegt (T3-Regel).
|
||
- Sicherheit hat Priorität 1. Ein Muster, gemeinsame Helfer. Jedes Feld bleibt bearbeitbar (Null-Regel, Annahme 20).
|
||
- Deutsche Oberfläche, kurze Sätze, ADHS-freundlich: ein Hauptknopf je Ansicht, wenig kognitive Last. WCAG 2.2 AA.
|
||
|
||
### Antworten des Inhabers auf die Spec-Fragen (2026-10-13), bindend
|
||
|
||
1. **Volksstärke** an der Volkskarte = besetzte Waben, 0–40, dieselbe Einheit wie in der Durchsicht. Alte Werte (Skala 1–10) werden nicht umgerechnet. Sie bleiben mit ihrer Einheit lesbar (4.14).
|
||
2. **Wetter für die GTS:** freie Open-Meteo-API. Der Inhaber weiß, dass sie laut Anbieter nur für nicht-kommerzielle Nutzung frei ist, und trägt dieses Risiko. Die Quelle steckt hinter einem kleinen Adapter, damit ein Wechsel später eine Einstellung ist (6.3).
|
||
3. **Erinnerungen:** beides, eine Meldung je fälliger Aufgabe **und** eine Morgenübersicht je Nutzer. Zugestellt in der App **und** über einen Telegram-Bot als neuen Kanal (Abschnitt 9a).
|
||
|
||
### Entscheidungen
|
||
|
||
| Frage | Entscheidung |
|
||
|---|---|
|
||
| Wie werden Vorschläge gespeichert? | **Eigenes Modell `Suggestion`**, kein neuer `TaskStatus`. Eine Aufgabe bleibt „was ich mir vorgenommen habe“. Kalender, Webhooks, Export, Suche und Offline-Paket sehen nur echte Aufgaben. Ein Vorschlag trägt Regel, Begründung, Belege, Anker und Entscheidung. Diese Felder gehören nicht an `Task`. |
|
||
| Ein oder zwei Eingänge? | **Ein Eingang „Vorschläge“** für Regel-, Ketten- und Autopilot-Vorschläge. Zwei Arten: `PLAN` („Übernehmen“ legt eine Aufgabe mit Termin an) und `DO` („Erfassen“ öffnet das Formular; der Vorschlag erledigt sich, sobald der Datensatz da ist). |
|
||
| Woher kommen Titel und Begründung? | Aus `reasonCode` plus `evidence` (nur IDs, Zahlen, Daten), **beim Lesen serverseitig gerendert**. So bleibt „vor 9 Tagen“ immer aktuell, und Texte lassen sich ohne Datenmigration verbessern. |
|
||
| Wie entsteht der Autopilot? | Regeltabelle im Code (Abschnitt 8), Werte in `AutomationRule.config`. Täglicher Abgleich aller Völker im Job und sofortiger Abgleich je Volk in einem Handler nach jedem fachlichen Ereignis. |
|
||
| Woher kommt die Saison? | Je Stand die weiteste von drei Quellen: eigene Meldung („Salweide blüht“), Schätzung aus der Grünlandtemperatursumme (GTS), Kalender-Rückfall. Feste GTS-Schwelle nur dort, wo der DWD sie definiert (200 °C). Spätere Schwellen lernt die App aus den eigenen Meldungen der Vorjahre, statt sie zu erfinden. |
|
||
| Wer holt die Wetterdaten? | **Eine Worker-Schleife.** Der Worker darf schon heute über 443 ins Internet. Jobs bleiben ohne Internet (NetworkPolicy unverändert). Koordinaten gehen auf 0,01° gerundet hinaus. |
|
||
| Welche Wetterquelle? | **Inhaber-Entscheidung: freie Open-Meteo-API**, Risiko der Nutzungsbedingungen (nur nicht-kommerziell) bewusst übernommen. Zugriff nur über den Adapter `WeatherHistorySource`; Wechsel zu bezahltem Open-Meteo per Einstellung, zu DWD/Bright Sky per neuem Adapter plus Einstellung (6.3). |
|
||
| Volksstärke | **Besetzte Waben, 0–40**, an Volkskarte und Durchsicht gleich. Neues Feld `Colony.occupiedFrames`. Der alte Wert `Colony.strength` (Skala 1–10) bleibt unverändert gespeichert und heißt in der Oberfläche „Volksstärke (alt, Skala 1–10)“. Keine Umrechnung (4.14). |
|
||
| Erinnerungen | **Je fällige Aufgabe eine Meldung und je Nutzer eine Morgenübersicht** (Inhaber-Entscheidung). Beides in der App und, wenn der Nutzer es einschaltet, über Telegram. |
|
||
| Telegram-Zustellung | **Outbox `NotificationDelivery`** wie `WebhookDelivery`. Handler und Jobs schreiben nur Zeilen. Eine Worker-Schleife sendet. Eine zweite empfängt `/start` und `/stop` per **Long Polling**, also ohne öffentlichen Eingang (Abschnitt 9a). |
|
||
| Alter einer Königin | **Eine Schwelle: `queenMaxAgeYears = 2`** für Dashboard (heute 3), `QueenAged` und Autopilot. Ab dem zweiten vollen Jahr nach dem Schlupf steht die Königin in ihrer dritten Saison. Wer dann warnt, hat in dieser Saison noch Zeit für die Nachzucht. Bei 3 käme der Hinweis erst in der vierten Saison. Der Wert ist eine Annahme nach Imkerpraxis und einstellbar. |
|
||
| Durchsicht → Volkskarte | **Im selben Request wie die Durchsicht**, über `ColoniesService.updateIn(tx, …)`. Atomar, offline-fähig, ein Hauptknopf „Speichern“. Kein zweiter Dialog nach dem Speichern. |
|
||
| Futterzustand | **Ein Vokabular:** `Inspection.feedStatus` wird `ColonyCondition`, wie an der Volkskarte. |
|
||
| Menge der Honigcharge | `HoneyBatch.amount` bleibt der aktuelle Rest, damit API und Shop stabil bleiben. Neu: `initialAmount` und Buchungen im Bewegungsbuch. Jede Änderung von `amount` läuft nur über `HoneyBatchLedgerService`. |
|
||
| Wartezeit und Plausibilität | Warnungen in der Antwort plus eine Meldung je Konflikt. Nie eine Sperre (Annahme 10). Einzige Ausnahme: Wassergehalt über der gesetzlichen Grenze verlangt beim Abfüllen eine Bestätigung (§10). |
|
||
| Benachrichtigungen je Nutzer | **Bewusste Abweichung von Rückgrat-Spec §4** (Checkliste „`NotificationRead` je Nutzer“): **je Nutzer eine eigene Zeile (Fan-out) mit Rechteprüfung beim Schreiben**, keine Tabelle `NotificationRead`. Grund: Heute sieht jeder angemeldete Nutzer jede Rundmeldung (`userId = null`), auch ohne Leserecht auf Völker oder Varroa. Eine Lese-Tabelle würde nur das gemeinsame `read`-Flag lösen, nicht dieses Leck. Fan-out löst beides in einem Schritt, braucht keinen Join beim Lesen und ist zugleich die Grundlage für die Zustellung je Nutzer (Telegram). |
|
||
| Rechte | Vorschläge nutzen die Aufgaben-Rechte (`tasks:read/create/update`). Neue Gruppe „Pflanzenentwicklung“ (`phenology:read/create/update`). Inhalte werden nach Leserechten gefiltert (Abschnitt 12). |
|
||
| Nachzügler des Rückgrats | `queens`, `extractions` und `honey-batches` stellen auf Ereignisse um, soweit T4 sie anfasst. Damit entfallen ihre Altaufrufe `webhooks.dispatch` und `calendarSync.enqueue`. |
|
||
|
||
## 2. Umfang
|
||
|
||
### 2.1 Im Umfang (§10 T4 → Lösung)
|
||
|
||
| Punkt aus §10 T4 | Lösung | Abschnitt |
|
||
|---|---|---|
|
||
| Durchsicht → Volkskarte, einzeln abwählbar, über `ColoniesService.update` | Abschnitt „Volkskarte“ im Durchsichtsformular, `applyToColony` im selben Request | 7.9 |
|
||
| Ein Vokabular für den Futterzustand | `Inspection.feedStatus` → `ColonyCondition`, Migration | 4.4 |
|
||
| Varroa-Ampel nach Jahreszeit (LWG), Jungvölker, nach Maßnahme 14 Tage | Bänder je Methode, Monat, Volkstyp; Regel `VARROA_THRESHOLD` erweitert | 7.1 |
|
||
| Fälligkeit der Durchsicht je Volk, Heute-Ansicht, §30 | Intervall je Saisonabschnitt, Karte „Durchsicht fällig“ mit „Runde starten“ | 7.2, 11 |
|
||
| Wartezeit-Kette: Spanne im Kalender, „Volk erntefähig“, Prüfung bei Ernte/Schleuderung/Abfüllung | `WithdrawalCheckService`, Warnungen, Meldungen, Kalenderquelle `WITHDRAWAL` | 7.5 |
|
||
| Ernte → Schleuderung → Charge vorbelegt, Plausibilität | Vorbelegungs-Endpunkte, Warnungen | 7.6 |
|
||
| `HoneyBatch.amount` nur über das Bewegungsbuch | `initialAmount`, `BatchMovement.honeyBatchId`, Ledger | 7.7 |
|
||
| Qualität vor der Abfüllung: Wassergehalt, MHD, Glasgröße | Regel `HONEY_WATER_CONTENT`, `Product.shelfLifeMonths` | 7.8 |
|
||
| Biotechnik-Fristen | Ketten-Vorschläge `QUEEN_CELLS_SWARM_CHECK`, `QUEEN_ACCEPTANCE_CHECK`, `SPLIT_SCHEDULE` | 7.4 |
|
||
| Volk beendet: Aufgaben anbieten, Königin-Zuordnung beenden (auch Auswinterung) | gemeinsamer Helfer `releaseQueenIn`, Aufräum-Vorschlag | 7.10 |
|
||
| Saison-Autopilot: nächste 3 Schritte mit Begründung, GTS oder Meldung, kein ML, täglich | Regeln A1–A10, Abgleich, Rangfolge | 8 |
|
||
| Vorschlagsliste mit „Übernehmen“/„Verwerfen“ | Modell `Suggestion`, Seite „Vorschläge“ | 9, 11.3 |
|
||
| §11 T4-Befunde: Automatik-Aufgaben fehlen im Kalender; Vorlagen nur beim Lesen; ein `read`-Flag; `amount` am Bewegungsbuch vorbei; kein Grenzwert Wassergehalt | Kalender und Vorlagen löst das Rückgrat (Tasks 9, 10, 15). T4 löst die übrigen drei. | 4.6, 4.8, 7.8 |
|
||
| Inhaber-Antwort 1: Volksstärke in besetzten Waben | `Colony.occupiedFrames`, alter Wert bleibt | 4.14 |
|
||
| Inhaber-Antwort 3: Erinnerungen je Aufgabe und Morgenübersicht, in der App und per Telegram | Handler `notifications.task-due`, Job-Schritt `digest`, Outbox `NotificationDelivery`, Telegram-Schleifen | 9a |
|
||
|
||
### 2.2 Befunde aus dem Code, die T4 behebt
|
||
|
||
| Befund | Lösung | Abschnitt |
|
||
|---|---|---|
|
||
| Rundmeldungen (`userId = null`) sieht jeder Nutzer, und alle teilen ein `read`-Flag | Fan-out je Nutzer mit Rechteprüfung; bewusste Abweichung von Rückgrat-Spec §4 | 4.8 |
|
||
| Stirbt ein Volk bei der Auswinterung (`markColonyDeceased`) oder wird es über „Bearbeiten“ auf `SOLD`/`DECEASED` gesetzt, bleibt die Königin zugeordnet. `dissolve` und `merge` beenden die Zuordnung schon. | ein Helfer `releaseQueenIn` für alle vier Wege, in derselben Transaktion | 5.4, 7.10 |
|
||
| Alter der Königin: Dashboard warnt ab 3 Jahren, `QueenAged` (Rückgrat) ab 2 | eine Schwelle `queenMaxAgeYears = 2` für alle | Entscheidungen, 6.2 |
|
||
| ARCHITECTURE hat die Annahmen 22 und 23 schon belegt, der T3-Plan plant 22–27 | Neue Annahmen nur unter der „nächsten freien Nummer“ zum Zeitpunkt des Baus | 15 |
|
||
|
||
### 2.3 Nicht im Umfang
|
||
|
||
- Wanderung, Stockwaage, Sensoren (Inhaber-Entscheidung; T7).
|
||
- Tierarzneimittel-Katalog, Etikett-Pflichtangaben, Behördenpaket (T6).
|
||
- Bienenjahr-Rad, Waben-Standkarte, Lebenslinie (T5).
|
||
- Mischungen, Importhonig, Backhonig-Logik (nur eigener deutscher Honig).
|
||
- Web-Push und E-Mail. Telegram ist der einzige zusätzliche Kanal.
|
||
- Telegram-Befehle außer `/start`, `/stop` und `/hilfe`. Kein Erfassen oder Abfragen über den Bot.
|
||
- Drohnenrahmen-Regeln, Ertragsprognose, Änderungen an der Trachtprognose.
|
||
- Bestand an Rohhonig vor der Charge (Ernte bis Schleuderung wird nicht gebucht).
|
||
|
||
### 2.4 Abgrenzung zu T3 und Rückgrat
|
||
|
||
| Baustein | Liefert | T4 ändert |
|
||
|---|---|---|
|
||
| T3 Task 2 `GET /varroa-measurements/threshold`, `varroaThresholdOf` | eine Schwelle (Milben/Tag) | ersetzt durch Bänder (`/thresholds`); `varroaThresholdOf` liest das neue Config-Format |
|
||
| T3 Task 11 `varroaLight(rate, threshold)` | gelb ab halber Schwelle | `varroaLight(value, band)` mit Bändern vom Server |
|
||
| T3 Task 14 `colonyStatusLight`, `INSPECTION_STALE_DAYS = 14` | feste 14 Tage | gelb ab Überschreiten von `inspectionDueOn` |
|
||
| T3 Task 3 `ColonyOverviewRow.openWarnings/openCritical` | aus Benachrichtigungen | aus offenen Vorschlägen (Priorität `HIGH`/`URGENT`) |
|
||
| T3 Task 9 Durchsichtsformular, `takeover.ts` | „Letztes Mal“, Freitext-Futter | Abschnitt „Volkskarte“; Freitext-Sonderfall entfällt nach Migration |
|
||
| T3 Task 3 `ColonyOverviewRow.strength`, `OfflinePackV1.colonies` | alte Skala 1–10 | zusätzlich `occupiedFrames`; `strength` bleibt als Altwert |
|
||
| T3 Task 1/9 `Inspection.colonyStrength` (besetzte Waben, `@Min(0)`) | ohne Obergrenze | `@Max(40)`, wie an der Volkskarte |
|
||
| Rückgrat Task 10 `triggerActionIn` | Aufgabe über `createInTx` | Vorschlag über `SuggestionsService.proposeIn` |
|
||
| Rückgrat Task 18 `INSPECTION_INTERVAL_DAYS = 21`, `ACTIVE_SEASON_MONTHS` | fester Takt | Intervall je Saisonabschnitt (7.2) |
|
||
|
||
## 3. Begriffe
|
||
|
||
- **Vorschlag** (`Suggestion`): eine Empfehlung zu genau einem Objekt (meist ein Volk). Art `PLAN` oder `DO`.
|
||
- **Schritt** (`NextStep`): ein Eintrag in „die nächsten 3 Schritte“. Quelle ist eine offene Aufgabe oder ein offener Vorschlag des Volks.
|
||
- **Anker**: die Sache, an der ein Vorschlag hängt, z. B. die ID der letzten Durchsicht oder das Jahr. Ein verworfener Vorschlag kommt für denselben Anker nicht wieder.
|
||
- **Phase** (`PhenologyPhase`): Stand der Pflanzenentwicklung, z. B. `KIRSCHE`.
|
||
- **Saisonabschnitt** (`SeasonSegment`): aus der Phase abgeleitet, z. B. `SCHWARMZEIT`. Er bestimmt Intervalle und Regeln.
|
||
- **Heute**: Kalendertag in `Europe/Berlin` (`berlinToday()`, neuer Helfer in `src/common/dates.ts`). Der Job läuft um 04:30 Berliner Zeit; der UTC-Tag von `dailyDedupeKey` ist dann derselbe Kalendertag.
|
||
|
||
## 4. Datenmodell
|
||
|
||
Alle Namen ladungstragend. Datumswerte wie bisher als `DateTime` (Mitternacht UTC), wie `Task.dueDate`.
|
||
|
||
### 4.1 Vorschläge
|
||
|
||
```prisma
|
||
enum SuggestionStatus { OPEN ACCEPTED DISMISSED RESOLVED EXPIRED OBSOLETE }
|
||
enum SuggestionMode { PLAN DO }
|
||
enum SuggestionSource { RULE CHAIN AUTOPILOT }
|
||
enum SuggestionIntent {
|
||
INSPECT SWARM_CHECK QUEEN_CHECK QUEEN_ACCEPTANCE QUEEN_REPLACE SPLIT_PLAN
|
||
VARROA_MEASURE VARROA_RECHECK VARROA_TREAT SUMMER_TREATMENT
|
||
FEED HONEY_SUPER WINTER_CHECK HEALTH_CHECK FOLLOW_UP STOCK_REORDER CLEANUP
|
||
}
|
||
|
||
model Suggestion {
|
||
id String @id @default(cuid())
|
||
ruleKey AutomationTriggerType
|
||
source SuggestionSource
|
||
mode SuggestionMode
|
||
intent SuggestionIntent
|
||
status SuggestionStatus @default(OPEN)
|
||
priority TaskPriority @default(NORMAL)
|
||
relatedType TaskRelatedType
|
||
relatedId String?
|
||
colonyId String?
|
||
colony Colony? @relation(fields: [colonyId], references: [id])
|
||
reasonCode String
|
||
evidence Json // nur IDs, Zahlen, YYYY-MM-DD
|
||
requiredPermission String? // z. B. 'varroa:read'; fehlt das Recht, ist der Vorschlag unsichtbar
|
||
dueDate DateTime?
|
||
expiresAt DateTime?
|
||
plan Json? // PLAN mit mehreren Terminen: [{ step, offsetDays, windowDays? }]
|
||
dedupeKey String? @unique
|
||
sourceEventId String?
|
||
decidedAt DateTime?
|
||
decidedByUserId String?
|
||
decidedByUser User? @relation("SuggestionDecider", fields: [decidedByUserId], references: [id])
|
||
decisionNote String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
tasks Task[]
|
||
|
||
@@index([status, colonyId])
|
||
@@index([status, dueDate])
|
||
@@index([ruleKey, relatedType, relatedId, status])
|
||
}
|
||
```
|
||
|
||
- `Task` bekommt `sourceSuggestionId String?` (Relation, Index). `sourceAutomationTrigger` bleibt und wird beim Übernehmen gesetzt.
|
||
- `dedupeKey` ist nullbar und `@unique`, wie `DomainEvent.dedupeKey`. Kein partieller Index (Rückgrat-Spec §3.5).
|
||
- Format: `<ruleKey>:<relatedId>:<anker>`, z. B. `INSPECTION_INTERVAL:col_1:insp_9`.
|
||
|
||
### 4.2 Pflanzenentwicklung
|
||
|
||
```prisma
|
||
enum PhenologyPhase { SALWEIDE KIRSCHE APFEL HOLUNDER LINDE TRACHTENDE HERBST WINTER }
|
||
enum SeasonSegment { WINTERRUHE FRUEHJAHR SCHWARMZEIT SOMMER SPAETSOMMER HERBST }
|
||
enum PhaseSource { REPORT GTS CALENDAR }
|
||
|
||
model PhenologyReport { // eigene Meldung per Antippen
|
||
id String @id @default(cuid())
|
||
apiaryId String? // null = alle Stände
|
||
apiary Apiary? @relation(fields: [apiaryId], references: [id])
|
||
phase PhenologyPhase
|
||
date DateTime
|
||
notes String?
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
revokedAt DateTime? // zurückgezogen statt gelöscht
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
@@index([date])
|
||
@@index([apiaryId, phase])
|
||
}
|
||
|
||
model ApiaryPhenology { // abgeleiteter Zustand, schreibt nur der Job/Handler
|
||
apiaryId String
|
||
year Int
|
||
gts Float?
|
||
gtsThroughDay DateTime?
|
||
missingDays Int @default(0)
|
||
estimatedPhase PhenologyPhase?
|
||
reportedPhase PhenologyPhase?
|
||
effectivePhase PhenologyPhase?
|
||
phaseSource PhaseSource?
|
||
segment SeasonSegment
|
||
segmentSince DateTime
|
||
updatedAt DateTime @updatedAt
|
||
@@id([apiaryId, year])
|
||
}
|
||
|
||
model ApiaryWeatherDay { // Tagesmittel je Stand, schreibt nur die Worker-Schleife
|
||
apiaryId String
|
||
day DateTime // Kalendertag Europe/Berlin
|
||
meanTempC Float?
|
||
source String // 'forecast' | 'archive'
|
||
fetchedAt DateTime @default(now())
|
||
@@id([apiaryId, day])
|
||
}
|
||
```
|
||
|
||
### 4.3 Ableger
|
||
|
||
- `enum SplitQueenSource { RAISE_OWN QUEEN_CELL QUEEN }` (Nachschaffung, reife Weiselzelle, Königin zugesetzt).
|
||
- `ColonyLineage.queenSource SplitQueenSource?`. `null` heißt „unbekannt“. Dann gibt es keine Kette, nichts wird angenommen.
|
||
- `SplitColonyDto` bekommt `date?` (heute: fest `new Date()`) und `queenSource?`.
|
||
|
||
### 4.4 Futter-Vokabular
|
||
|
||
- `Inspection.feedStatus String?` → `ColonyCondition?`. Volkskarte und Durchsicht sprechen dieselbe Sprache.
|
||
- Abbildung (ein Helfer `feedConditionFromLegacy` in `src/common/feed-vocabulary.ts`, auch für den Backup-Import alter Bundles):
|
||
|
||
| alter Wert (getrimmt, ohne Groß-/Kleinschreibung) | neu |
|
||
|---|---|
|
||
| `ausreichend` | `GOOD` |
|
||
| `niedrig` | `WEAK` |
|
||
| `kritisch` | `CRITICAL` |
|
||
| leer | `null` |
|
||
| jeder andere Text | `null`, Text wandert an das Ende von `notes` als „Futter (alt): …“ |
|
||
|
||
- Labels für Futter überall gleich (`FEED_CONDITION_LABELS`): `EXCELLENT` „Reichlich“, `GOOD` „Ausreichend“, `AVERAGE` „Mittel“, `WEAK` „Niedrig“, `CRITICAL` „Kritisch“.
|
||
- Das Durchsichtsformular bietet wie bisher drei Werte plus „Nicht geprüft“. Ein gespeicherter Wert außerhalb dieser drei erscheint im Bearbeiten-Dialog trotzdem.
|
||
|
||
### 4.5 Varroa-Probe
|
||
|
||
- `VarroaMeasurement.sampleBees Int?` (Probengröße in Bienen).
|
||
- Bei `POWDERED_SUGAR` und `WASHING_METHOD` wird `calculatedValue` = `miteCount / sampleBees × 100` (Befall in %), sonst wie bisher. Die Regel für manuelle Werte (Annahme 12) gilt unverändert.
|
||
- Das Formular bietet den Chip „500 Bienen (≈ 50 g)“, vorgewählt ist nichts (wie T3 bei den Tagen).
|
||
|
||
### 4.6 Bewegungsbuch der Honigcharge
|
||
|
||
- `HoneyBatch.initialAmount Float?` (kg, Ausgangsmenge).
|
||
- `BatchMovement.honeyBatchId String?` (FK, `onDelete: Restrict`) und `BatchMovement.sourceBottlingBatchId String?` (FK, `onDelete: SetNull`, Rückverweis auf die verursachende Abfüllung).
|
||
- `BatchMovementType` + `BOTTLING`.
|
||
- `quantity` ist bei Honig in Gramm (Int), mit Vorzeichen.
|
||
- CHECK-Constraint neu: `num_nonnulls("bottlingBatchId","productionBatchId","rawMaterialLotId","honeyBatchId") = 1`.
|
||
- Invariante: `amount = initialAmount + Σ quantity / 1000`, gerundet auf Gramm. Ist `initialAmount` `null`, wird nicht gebucht (Menge nicht erfasst, wie heute).
|
||
|
||
### 4.7 Produkt
|
||
|
||
- `Product.shelfLifeMonths Int?` (Haltbarkeit in Monaten, kein Standardwert). `packagingSize` gilt als Gramm wie `BottlingBatch.jarSize`; das prüft der Bau am Produktformular.
|
||
|
||
### 4.8 Benachrichtigungen
|
||
|
||
Bewusste Abweichung von Rückgrat-Spec §4: Fan-out statt `NotificationRead` (Begründung in den Entscheidungen).
|
||
|
||
- `Notification.userId` wird Pflicht. Rundmeldungen (`userId = null`) gibt es nicht mehr.
|
||
- `Notification.dedupeKey String? @unique`. Format `<art>:<objekt…>:<userId>`, bei täglichen Meldungen mit Tag, z. B. `withdrawal-ended:<treatmentId>:<userId>` oder `job-failed:<job>:<YYYY-MM-DD>:<userId>`.
|
||
- `Notification.category NotificationCategory` (neu): `TASK_DUE`, `DIGEST`, `SUGGESTION`, `WITHDRAWAL`, `SEASON`, `SYSTEM`. Altzeilen bekommen `SUGGESTION` (Automatik) bzw. `SYSTEM` (Job-Fehler).
|
||
- `Notification.link String?`: relativer App-Pfad (beginnt mit `/`, kein `//`, kein Schema), z. B. `/colonies/<id>`. Wird beim Schreiben geprüft.
|
||
- `Notification.requiredPermissions String[] @default([])`: alle Leserechte, die der Inhalt voraussetzt (die Morgenübersicht nutzt mehrere). Der Telegram-Versand prüft sie beim Senden erneut (9a.6).
|
||
- `NotificationsService.record` verlangt `userId`. Neu `recordForUsers(userIds, input, tx)`; `recordForPermission` bleibt der Normalweg und vergibt den Schlüssel je Empfänger. Beide legen in derselben Transaktion die Telegram-Zustellungen an (9a.4).
|
||
|
||
### 4.9 Kalender
|
||
|
||
- `CalendarSyncSourceType` + `WITHDRAWAL` (Quelle: Behandlungs-ID).
|
||
- `CalendarSyncSettings.syncWithdrawal Boolean @default(true)`.
|
||
- `GraphCalendarEventInput` bekommt optional `endDate` (ganztägig, mehrtägig; Graph-Ende exklusiv).
|
||
|
||
### 4.10 Rechte
|
||
|
||
- Neue Gruppe „Pflanzenentwicklung“, Ressource `phenology`: `read` „Ansehen“, `create` „Melden“, `update` „Bearbeiten“.
|
||
- Die Migration vergibt sie wie bei `winter-checks` an Nicht-System-Rollen (Annahme 16): `read` an Rollen mit `colonies:read`, `create`/`update` an Rollen mit `inspections:create`.
|
||
|
||
### 4.11 Regeln (`AutomationTriggerType`)
|
||
|
||
Neue Werte, alle mit Eintrag in `DEFAULT_RULES`, Config-Parser und Texten:
|
||
|
||
| Trigger | Art | Standard aktiv | Config (Standard) |
|
||
|---|---|---|---|
|
||
| `INSPECTION_INTERVAL` | Autopilot-Intervall | ja | Tage je Abschnitt, siehe 7.2 |
|
||
| `QUEEN_CELLS_SWARM_CHECK` | Kette | ja | `{ dueInDays: 7 }` |
|
||
| `QUEEN_ACCEPTANCE_CHECK` | Kette | ja | `{ dueInDays: 7 }` |
|
||
| `SPLIT_SCHEDULE` | Kette | ja | Schritte je `queenSource`, siehe 7.4 |
|
||
| `COLONY_ENDED_CLEANUP` | Kette | ja | `{}` |
|
||
| `WITHDRAWAL_ENDED` | Meldung | ja | `{ notify: true }` |
|
||
| `HONEY_WATER_CONTENT` | Qualität | ja | siehe 7.8 |
|
||
| `SEASON_AUTOPILOT` | Autopilot | ja | siehe 8.2 |
|
||
| `PHENOLOGY` | Saison | ja | siehe 7.3 |
|
||
|
||
- `VARROA_THRESHOLD` behält seinen Namen, die Config wird zum Bänder-Format (7.1). Ein alter Wert `thresholdMitesPerDay` wird als `naturalFall.redAbove` gelesen.
|
||
- Jede Regel bekommt einen Parser `parse<Trigger>Config(json)` in `src/automation/rule-config/`. Ungültige Werte fallen auf den Standard zurück, wie `varroaThresholdOf`. `PATCH /automation/rules/:id` lehnt ungültige bekannte Schlüssel mit 400 ab.
|
||
- Bestehende vorschlagende Regeln bekommen `config.notify` (Standard: `true` für `QUEENLESS_NO_EGGS`, `VARROA_THRESHOLD`, `COLONY_HEALTH_CRITICAL`; sonst `false`).
|
||
|
||
### 4.12 Migrationen
|
||
|
||
Zeitstempel vergibt der Bau. Die Reihenfolge ist bindend. Enum-Werte kommen in eigene Migrationen vor jeder Datenänderung, die sie benutzt: Postgres kann einen neuen Enum-Wert erst nach dem Commit verwenden.
|
||
|
||
| Nr | Name | Inhalt | Prüfung danach |
|
||
|---|---|---|---|
|
||
| 1 | `<ts>_t4_enum_values` | `AutomationTriggerType` (9 Werte), `BatchMovementType.BOTTLING`, `CalendarSyncSourceType.WITHDRAWAL` | `migrate diff --exit-code` |
|
||
| 2 | `<ts>_t4_suggestions` | Enums und `Suggestion`, `Task.sourceSuggestionId` | Wächter Backup |
|
||
| 3 | `<ts>_t4_feed_vocabulary` | neue Spalte, Abbildung nach 4.4, Freitext nach `notes`, alte Spalte weg, Umbenennen | SQL: Zahl umgezogener Freitexte im Migrationslog; keine Zeile mit Text und ohne Notiz |
|
||
| 4 | `<ts>_t4_phenology` | Enums, drei Modelle, Rechte und Rollen-Zuordnung | Rechte-Abdeckung e2e |
|
||
| 5 | `<ts>_t4_split_queen_source` | `SplitQueenSource`, `ColonyLineage.queenSource` | – |
|
||
| 6 | `<ts>_t4_honey_ledger` | Spalten, CHECK neu, Rückrechnung (siehe unten) | Invarianten-Abfrage liefert 0 Zeilen |
|
||
| 7 | `<ts>_t4_quality_fields` | `VarroaMeasurement.sampleBees`, `Product.shelfLifeMonths` | Null-Regel-Spec |
|
||
| 8 | `<ts>_t4_notification_fanout` | Rundmeldungen je aktivem Nutzer kopieren (mit gleichem `read`), Originale löschen, `userId NOT NULL`, `dedupeKey` | keine Zeile mit `userId IS NULL` |
|
||
| 9 | `<ts>_t4_calendar_withdrawal` | `syncWithdrawal` | – |
|
||
| 10 | `<ts>_t4_occupied_frames` | `Colony.occupiedFrames Int?` mit CHECK 0–40; keine Datenänderung an `strength` | Prüfabfrage: keine `Inspection.colonyStrength` über 40 (sonst vor dem Deploy klären, nicht kürzen) |
|
||
| 11 | `<ts>_t4_notification_channels` | `NotificationCategory`, `Notification.category/link/requiredPermissions`, `NotificationPreference`, `TelegramLink`, `TelegramLinkToken`, `NotificationDelivery`, `TelegramPollState` | Wächter Backup; `migrate diff --exit-code` |
|
||
|
||
Migration 8 läuft vor 11. Migration 11 belegt `category` der Altzeilen nach der Regel aus 4.8 und setzt danach `NOT NULL`.
|
||
|
||
Rückrechnung in Migration 6, je Charge mit `amount IS NOT NULL`:
|
||
1. `initialAmount = amount + Σ totalAmount` aller Abfüllungen der Charge.
|
||
2. Je Abfüllung eine Buchung `BOTTLING` mit `−round(totalAmount × 1000)` g, `sourceBottlingBatchId`, `createdAt` der Abfüllung, Notiz „Übernahme aus Abfüllung (T4)“.
|
||
|
||
Frühere Handkorrekturen von `amount` stecken danach in `initialAmount`. Das steht in der Notiz der Charge nicht; es steht in ARCHITECTURE (Annahme „Bewegungsbuch der Honigcharge“).
|
||
|
||
### 4.13 Backup
|
||
|
||
| Modell | Backup | Begründung (englisch in `MODELS_EXCLUDED_FROM_BACKUP`) |
|
||
|---|---|---|
|
||
| `Suggestion` | ja | Entscheidungen des Imkers; ohne sie kämen verworfene Vorschläge nach einem Restore wieder |
|
||
| `PhenologyReport` | ja | eigene Beobachtungen |
|
||
| `ApiaryPhenology` | nein | 'Derived season state, recomputed by daily-evaluation.' |
|
||
| `ApiaryWeatherDay` | nein | 'Weather cache, refetched by the worker.' |
|
||
| `NotificationPreference` | ja | Einstellungen des Nutzers |
|
||
| `TelegramLink` | nein | 'Chat binding to this deployment\'s bot - users re-link after a restore (like ApiKey).' |
|
||
| `TelegramLinkToken` | nein | 'Single-use link tokens, valid for minutes.' |
|
||
| `NotificationDelivery` | nein | 'Delivery queue - restoring it would resend old messages.' |
|
||
| `TelegramPollState` | nein | 'Update offset and poll lease of this deployment.' |
|
||
|
||
Der Import alter Bundles bildet ab: Futter-Freitext (4.4), `Notification` ohne `userId` (Fan-out wie Migration 8) und ohne `category` (wie Migration 11), `HoneyBatch` ohne `initialAmount` (Rückrechnung wie Migration 6). `Colony.strength` und `Colony.occupiedFrames` werden unverändert übernommen.
|
||
|
||
### 4.14 Volksstärke (Inhaber-Antwort 1)
|
||
|
||
- **Neu:** `Colony.occupiedFrames Int?`, Einheit besetzte Waben, 0–40. Dieselbe Einheit und dieselben Grenzen wie `Inspection.colonyStrength` (dort kommt `@Max(40)` hinzu).
|
||
- **Alt:** `Colony.strength` (1–10, ohne Einheit) bleibt mit allen Werten, wie sie sind. Keine Umrechnung: Eine „7 von 10“ lässt sich nicht verlässlich in Waben übersetzen.
|
||
- Bedeutung ab T4:
|
||
- `strength` heißt in der Oberfläche „Volksstärke (alt, Skala 1–10)“. Das Bearbeiten-Formular zeigt es nur, wenn ein Wert gespeichert ist (wie „Brutmenge (alt)“ in T3). Leeren geht (Null-Regel), Ändern geht im Bereich 1–10.
|
||
- Neue Völker bekommen im Formular nur `occupiedFrames`. Die API nimmt `strength` weiter an, damit nichts bricht; Swagger markiert es als veraltet.
|
||
- Anzeige auf der Volksseite: „{n} besetzte Waben“. Fehlt der neue Wert, aber der alte ist da: „Volksstärke (alt): 7 von 10“.
|
||
- Historie bleibt lesbar: Audit-Einträge und Exporte behalten die alten Werte mit ihrem alten Feldnamen. CSV-Export/-Import und Backup führen beide Spalten.
|
||
- Der Autopilot (A7) und die Statistik „Entwicklung Volksstärke“ nutzen die Durchsichten (`colonyStrength`), nicht die Volkskarte.
|
||
|
||
### 4.15 Benachrichtigungskanäle (Inhaber-Antwort 3)
|
||
|
||
```prisma
|
||
enum NotificationCategory { TASK_DUE DIGEST SUGGESTION WITHDRAWAL SEASON SYSTEM }
|
||
enum NotificationChannel { TELEGRAM }
|
||
enum NotificationDeliveryStatus { PENDING PROCESSING DELIVERED SKIPPED FAILED }
|
||
|
||
model NotificationPreference { // je Nutzer, Standard beim ersten Lesen angelegt
|
||
userId String @id
|
||
user User @relation(fields: [userId], references: [id])
|
||
taskDueEnabled Boolean @default(true) // Meldung je fälliger Aufgabe
|
||
digestEnabled Boolean @default(true) // Morgenübersicht
|
||
digestTime String @default("06:30") // HH:MM, Europe/Berlin, 05:00–12:00
|
||
quietFrom String? @default("21:00") // Ruhezeit für Telegram; null = keine
|
||
quietUntil String? @default("06:30")
|
||
telegramEnabled Boolean @default(false)
|
||
telegramCategories NotificationCategory[] @default([TASK_DUE, DIGEST])
|
||
telegramDetail Boolean @default(true) // false: nur „Neue Meldung in ApiCore“ plus Link
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
model TelegramLink {
|
||
userId String @id
|
||
user User @relation(fields: [userId], references: [id])
|
||
chatId BigInt @unique // privater Chat; ein Chat gehört zu genau einem Nutzer
|
||
telegramFirstName String? // nur zur Anzeige „Verbunden mit …“
|
||
linkedAt DateTime @default(now())
|
||
}
|
||
|
||
model TelegramLinkToken {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
tokenHash String @unique // SHA-256 des Tokens, nie der Klartext
|
||
expiresAt DateTime // 10 Minuten
|
||
consumedAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
@@index([userId])
|
||
}
|
||
|
||
model NotificationDelivery { // Outbox wie WebhookDelivery
|
||
id String @id @default(cuid())
|
||
channel NotificationChannel
|
||
userId String? // gesetzt bei Meldungen; Chat wird beim Senden aus TelegramLink gelesen
|
||
user User? @relation(fields: [userId], references: [id])
|
||
chatId BigInt? // nur für Systemantworten an einen Chat (auch unverbundene)
|
||
notificationId String?
|
||
notification Notification? @relation(fields: [notificationId], references: [id], onDelete: Cascade)
|
||
templateCode String? // Systemantworten des Bots (Verbunden, Ungültig, Abgemeldet, Hilfe)
|
||
status NotificationDeliveryStatus @default(PENDING)
|
||
attempts Int @default(0)
|
||
availableAt DateTime @default(now())
|
||
claimedAt DateTime?
|
||
lastStatusCode Int?
|
||
lastError String?
|
||
deliveredAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
@@index([status, availableAt])
|
||
@@index([userId, createdAt])
|
||
}
|
||
|
||
model TelegramPollState { // eine Zeile: Offset und Lease des Empfängers
|
||
id String @id @default("singleton")
|
||
updateOffset BigInt @default(0)
|
||
leaseOwner String?
|
||
leaseUntil DateTime?
|
||
lastPollAt DateTime?
|
||
lastError String?
|
||
}
|
||
```
|
||
|
||
- Der Text einer Zustellung wird **nicht** gespeichert. Der Versand rendert ihn aus der `Notification` bzw. aus `templateCode` (9a.6).
|
||
- CHECK-Constraint: genau eins von (`notificationId` mit `userId`) oder (`templateCode` mit `chatId`).
|
||
- Ruhezeit und Morgenzeit sind Annahmen (Standard 21:00–06:30 bzw. 06:30) und je Nutzer änderbar.
|
||
- `chatId` ist kein Geheimnis (ohne Bot-Token nutzlos), aber personenbezogen. Darum nicht im Backup und nie in Logs.
|
||
|
||
## 5. Ereignisse und Handler
|
||
|
||
### 5.1 Neue Ereignisse
|
||
|
||
T4 erweitert `DomainEventPayloads` bewusst. Der Kommentar „vollständig“ im Rückgrat-Plan meint dessen Stufe.
|
||
|
||
| Ereignis | Payload | Aggregat | Erzeuger |
|
||
|---|---|---|---|
|
||
| `ColonySplit` | `{ parentColonyId, lineageId, queenSource: SplitQueenSource \| null, splitOn }` | `Colony` (Ableger) | `ColoniesService.split` |
|
||
| `QueenAssigned` | `{ colonyId, historyId, assignedOn, previousColonyId: string \| null }` | `Queen` | `QueensService.assignColony` |
|
||
| `ExtractionRecorded` / `ExtractionCorrected` | `{ harvestIds: string[] }` | `Extraction` | `ExtractionsService` |
|
||
| `HoneyBatchRecorded` / `HoneyBatchCorrected` | `{ extractionId }` | `HoneyBatch` | `HoneyBatchesService` |
|
||
| `PhenologyReported` / `PhenologyCorrected` | `{ apiaryId: string \| null, phase, reportedOn }` | `PhenologyReport` | `PhenologyService` |
|
||
| `SeasonPhaseChanged` (zeitbasiert) | `{ apiaryId, phase, segment, previousPhase, previousSegment, source }` | `Apiary` | `daily-evaluation`, Handler `phenology.recompute` |
|
||
|
||
- `AggregateType` + `Extraction`, `HoneyBatch`, `PhenologyReport`, `Apiary`.
|
||
- `SeasonPhaseChanged` hat `dedupeKey = dailyDedupeKey('SeasonPhaseChanged', apiaryId, Tag)`.
|
||
- Payloads tragen nur IDs, Enum-Werte und Daten. Keine Namen, keine Notizen.
|
||
|
||
### 5.2 Handler
|
||
|
||
| Ereignis | Erzeuger | Handler | Wirkung |
|
||
|---|---|---|---|
|
||
| `InspectionRecorded` | inspections | `automation.inspection-rules` (Rückgrat, geändert) | `QUEENLESS_NO_EGGS`, `FEED_STATUS_CRITICAL` (jetzt `feedStatus = CRITICAL`) → Vorschlag statt Aufgabe |
|
||
| `InspectionRecorded` | inspections | `chains.queen-cells` | `queenCellsPresent = true` → PLAN „Schwarmkontrolle“, fällig +7 |
|
||
| `InspectionRecorded`, `InspectionCorrected` | inspections | `autopilot.colony-sync` | Schritte des Volks neu abgleichen |
|
||
| `VarroaMeasured`, `VarroaCorrected` | varroa | `automation.varroa-threshold` (geändert) | Band rot → PLAN „Behandlung prüfen“; nicht bis 14 Tage nach Behandlungsende |
|
||
| `VarroaMeasured`, `VarroaCorrected` | varroa | `autopilot.colony-sync` | – |
|
||
| `TreatmentRecorded`, `TreatmentCorrected` | treatments | `automation.treatment-follow-up` (Rückgrat), `calendar-sync.withdrawal` (neu), `autopilot.colony-sync` | Wartefenster in die Kalender-Outbox |
|
||
| `FeedingRecorded`, `FeedingCorrected` | feedings | `automation.feeding-follow-up` (Rückgrat), `autopilot.colony-sync` | – |
|
||
| `HarvestRecorded`, `HarvestCorrected` | harvests | `withdrawal.harvest-check`, `autopilot.colony-sync` (je Volk) | WARNUNG je Konflikt (Volk, Ernte), dedupliziert |
|
||
| `ExtractionRecorded`, `ExtractionCorrected` | extractions | `withdrawal.extraction-check` | Meldung nur für noch nicht gemeldete Konflikte |
|
||
| `HoneyBatchRecorded` | honey-batches | `webhooks.forward` | Webhook `batch.created` (bisher Altpfad) |
|
||
| `BatchBottled`, `BottlingCorrected` | bottling | `withdrawal.bottling-check` | wie oben |
|
||
| `ColonyStatusChanged` | colonies | `automation.colony-health` (Rückgrat, geändert), `autopilot.colony-sync` | KRITISCH → Vorschlag |
|
||
| `ColonyEnded` | colonies, winter-checks | `suggestions.colony-ended` | offene Vorschläge → `OBSOLETE`; Aufräum-Vorschlag (7.10) |
|
||
| `ColonySplit` | colonies | `chains.split-schedule`, `autopilot.colony-sync` (beide Völker) | PLAN mit Terminen je `queenSource` |
|
||
| `QueenAssigned` | queens | `chains.queen-acceptance`, `calendar-sync.queen-assignment`, `webhooks.forward`, `autopilot.colony-sync` | PLAN „Annahme prüfen“ +7; Kalender und Webhook `queen.replaced` statt Altpfad |
|
||
| `TaskCompleted`, `TaskUpdated` | tasks | `autopilot.colony-sync` (nur `relatedType = COLONY`) | – |
|
||
| `WithdrawalEnded` | daily-evaluation | `notifications.withdrawal-ended`, `autopilot.colony-sync` | INFO „B3 ist wieder erntefähig“ an `harvests:create`, nur wenn kein anderes Fenster mehr läuft |
|
||
| `QueenAged`, `InspectionOverdue`, `VarroaMeasurementDue` | daily-evaluation | `autopilot.colony-sync` | – |
|
||
| `TaskDue` | daily-evaluation | `notifications.task-due` | HINWEIS „Heute fällig: {Titel}“ mit Link an den Zuständigen, ohne Zuständigen an Inhaber von `tasks:complete`; nur bei `taskDueEnabled`; Schlüssel `task-due:<taskId>:<Tag>:<userId>`; Telegram-Zustellung nach 9a.4 |
|
||
| `PhenologyReported`, `PhenologyCorrected` | phenology | `phenology.recompute` | Zustand des Stands bzw. aller Stände neu; schreibt bei Wechsel `SeasonPhaseChanged` |
|
||
| `SeasonPhaseChanged` | daily-evaluation, phenology | `autopilot.apiary-sync`, `notifications.season-segment` | alle Völker des Stands; INFO nur bei Wechsel des Abschnitts |
|
||
|
||
### 5.3 Regeln für alle T4-Handler
|
||
|
||
- Handler-IDs folgen `HANDLER_ID_PATTERN`. Alle stehen in Modulen, die `WorkerModule` importiert (Carry-forward Rückgrat Task 6).
|
||
- Nur Schreiben über `tx`, keine Netzwerkaufrufe (Wächter `handler-contract.spec.ts`).
|
||
- `autopilot.colony-sync` sperrt zuerst die Volkszeile (`SELECT … FROM "Colony" WHERE id = $1 FOR UPDATE`). Zwei Ereignisse desselben Volks in zwei Workern laufen so nacheinander.
|
||
- Vorschläge entstehen über `createMany({ skipDuplicates: true })` mit `dedupeKey`. Eine erneute Zustellung erzeugt nichts doppelt.
|
||
- Ein Handler für ein beendetes Volk tut nichts (außer `suggestions.colony-ended`).
|
||
|
||
### 5.4 Erzeuger-Umstellungen
|
||
|
||
- `ColoniesService.updateIn(tx, id, dto, actingUserId)` wird der gemeinsame Kern von `update`. Ihn nutzt auch die Durchsicht (7.9).
|
||
- `releaseQueenIn(tx, colonyId, endDate)` in `src/colonies/colony-lifecycle.ts`: offene `QueenColonyHistory` mit `endDate` beenden, `currentQueenId = null`. Pflicht in **jedem** Weg, auf dem ein Volk endet:
|
||
|
||
| Weg | heute | T4 |
|
||
|---|---|---|
|
||
| `dissolve` (`DISSOLVED`) | beendet die Zuordnung | über den Helfer |
|
||
| `merge` (`MERGED`) | beendet die Zuordnung | über den Helfer |
|
||
| `update` auf `SOLD` oder `DECEASED` | **Königin bleibt zugeordnet** | über den Helfer, Enddatum = Tag der Änderung |
|
||
| Auswinterung mit `markColonyDeceased` (`DECEASED`) | **Königin bleibt zugeordnet** | über den Helfer, Enddatum = Datum der Auswinterung |
|
||
|
||
Ein Wächter-Test prüft: Jede Stelle, die `status` auf einen Wert aus `TERMINAL_COLONY_STATUSES` setzt, ruft `releaseQueenIn` in derselben Transaktion auf.
|
||
- `ColoniesService.split` schreibt `ColonySplit`, Audit in der Transaktion.
|
||
- `QueensService.assignColony` schreibt `QueenAssigned`; `webhooks.dispatch('queen.replaced')` und `calendarSync.enqueue('QUEEN_ASSIGNMENT')` entfallen.
|
||
- `ExtractionsService` und `HoneyBatchesService`: Audit in die Transaktion, Ereignisse wie in 5.1. `webhooks.dispatch('batch.created')` entfällt.
|
||
- Wächter `after-commit-usage.spec.ts`: Die Liste der Altpfade verliert `queens` und `honey-batches`.
|
||
|
||
## 6. daily-evaluation und Wetter-Schleife
|
||
|
||
### 6.1 Ablauf des Jobs
|
||
|
||
Der Job aus Rückgrat Task 18 bekommt Schritte. Jeder Schritt läuft in eigenen Transaktionen. Scheitert einer, laufen die anderen; `JobRun` ist dann `FAILED` mit den Zählern.
|
||
|
||
| Schritt | Inhalt | Transaktion |
|
||
|---|---|---|
|
||
| 1 `phenology` | je aktivem Stand GTS, Phasen, Abschnitt berechnen (7.3), `ApiaryPhenology` schreiben, bei Wechsel `SeasonPhaseChanged` | je Stand |
|
||
| 2 Zeit-Ereignisse | `WithdrawalEnded`, `TaskDue`, `QueenAged`, `VarroaMeasurementDue`, `InspectionOverdue` mit den Änderungen aus 6.2 | je Typ (wie Rückgrat) |
|
||
| 3 `autopilot` | je lebendem Volk `AutopilotService.syncColonyIn(tx, colonyId, today)` | je Volk, Volkszeile gesperrt |
|
||
| 4 `suggestions-expiry` | `OPEN` mit `expiresAt < today` → `EXPIRED` | eine Anweisung |
|
||
| 5 `digest` | je aktivem Nutzer mit `digestEnabled` eine Morgenübersicht (9a.3) samt Telegram-Zustellung zur eingestellten Uhrzeit | je Nutzer |
|
||
|
||
`JobResult` zusätzlich: `phenologyApiaries`, `seasonChanges`, `autopilotColonies`, `suggestionsOpened`, `suggestionsResolved`, `suggestionsExpired`, `digests`.
|
||
|
||
Schritt 5 läuft nach Schritt 3, damit die Übersicht die frischen Schritte kennt. `TaskDue` aus Schritt 2 verarbeitet der Worker gleich danach; die Meldungen je Aufgabe sind also vor der frühesten Übersichts-Uhrzeit (05:00) da.
|
||
|
||
### 6.2 Änderungen an den zeitbasierten Ereignissen
|
||
|
||
| Ereignis | Rückgrat heute | T4 |
|
||
|---|---|---|
|
||
| `InspectionOverdue` | Intervall 21 Tage, nur März–Oktober | Tag nach `inspectionDueOn()` (7.2); keine Ereignisse in `WINTERRUHE` |
|
||
| `VarroaMeasurementDue` | Intervall 28 Tage, nur März–Oktober | Regel A2 (8.2): 28 Tage außerhalb `WINTERRUHE`, dazu Winterfenster |
|
||
| `QueenAged` | ab 2 Jahren, einmal im Jahr | Schwelle aus `SEASON_AUTOPILOT.queenMaxAgeYears` (Standard 2, Begründung in den Entscheidungen). `DashboardService` nutzt dieselbe Zahl; die Konstante `OLD_QUEEN_AGE_YEARS = 3` entfällt. |
|
||
| `WithdrawalEnded` | `(endDate ?? date) + withdrawalPeriodDays` | unverändert; Fensterrechnung aus dem gemeinsamen Helfer `withdrawal-window.ts` (auch für `getWithdrawalStatus`) |
|
||
|
||
`INSPECTION_INTERVAL_DAYS` und `ACTIVE_SEASON_MONTHS` entfallen.
|
||
|
||
### 6.3 Worker-Schleife `phenology-weather`
|
||
|
||
- Neue Zeile in der Loop-Tabelle von `WorkerRuntime`: Takt 60 min, kein Weckkanal, respektiert das Stop-Signal.
|
||
- Je Takt: für jeden aktiven Stand mit Koordinaten fehlende Tage vom 1. Januar bis gestern (Europe/Berlin) suchen.
|
||
- Fehlende Tage der letzten 90 Tage: Forecast-API mit `past_days=92`, `daily=temperature_2m_mean`, `timezone=Europe/Berlin`.
|
||
- Ältere Lücken: Archiv-API mit `start_date`/`end_date`.
|
||
- Koordinaten auf 0,01° gerundet. Stände mit demselben gerundeten Punkt teilen sich eine Anfrage.
|
||
- Feste Hosts aus Konstanten, Timeout 8 s, Antwortgröße begrenzt, Werte nur zwischen −50 und 50 °C, sonst `null`.
|
||
- Fehler: loggen über `loggableError()`, nächster Takt versucht es wieder. Kein Ereignis, keine Meldung.
|
||
- `cleanup` löscht `ApiaryWeatherDay` älter als 4 Kalenderjahre (für die Kalibrierung, 7.3).
|
||
- **Quelle (Inhaber-Entscheidung):** freie Open-Meteo-API. Laut Anbieter ist sie nur für nicht-kommerzielle Nutzung frei; der Inhaber trägt dieses Risiko bewusst. Unser Bedarf liegt weit unter der Tagesgrenze des Anbieters (eine Anfrage je gerundetem Punkt und Tag).
|
||
- **Adapter:** Die Schleife kennt nur `WeatherHistorySource.dailyMeans(point, fromDay, toDay): Promise<DailyMean[]>`. Welche Umsetzung läuft, bestimmen Umgebungsvariablen aus der ConfigMap:
|
||
|
||
| Variable | Standard | Zweck |
|
||
|---|---|---|
|
||
| `WEATHER_HISTORY_SOURCE` | `open-meteo` | Auswahl der Umsetzung |
|
||
| `OPEN_METEO_FORECAST_URL` | `https://api.open-meteo.com/v1/forecast` | Basis-URL; für das bezahlte Angebot die Kunden-URL |
|
||
| `OPEN_METEO_ARCHIVE_URL` | `https://archive-api.open-meteo.com/v1/archive` | dto |
|
||
| `OPEN_METEO_API_KEY` | leer | Secret; nur für das bezahlte Angebot |
|
||
|
||
- Wechsel zu bezahltem Open-Meteo: nur Einstellungen (URL und Schlüssel), kein Code.
|
||
- Wechsel zu DWD/Bright Sky: eine neue Umsetzung (etwa 0,5 PT), danach `WEATHER_HISTORY_SOURCE=bright-sky`.
|
||
- Die bestehende Wetteranzeige (`WeatherService`) liest dieselben URL- und Schlüssel-Variablen. Ein Wechsel des Angebots gilt so für beide.
|
||
- URLs nur `https:` und nur aus der Konfiguration, nie aus Nutzereingaben. Der Schlüssel erscheint nie in Logs oder `lastError` (URL wird vor dem Loggen entfernt).
|
||
|
||
## 7. Fachregeln
|
||
|
||
Alle Werte stehen in `AutomationRule.config` und sind in „Verwaltung > Automatisierung“ änderbar. „Annahme“ heißt: Standardwert ohne belastbare Quelle, vor dem Bau zu prüfen.
|
||
|
||
### 7.1 Varroa-Ampel nach Jahreszeit
|
||
|
||
| Methode | Zeitraum | Grün | Gelb | Rot | Quelle |
|
||
|---|---|---|---|---|---|
|
||
| Natürlicher Milbenfall (Milben/Tag) | März–Oktober | unter 5 | 5 bis 10 | über 10 | LWG Veitshöchheim, Tabelle aus Fahrplan §10; Gültigkeit ab März = Annahme |
|
||
| Natürlicher Milbenfall (Milben/Tag) | November–Februar | bis 0,5 | – | über 0,5 | LWG (Fahrplan §10: „ab November Behandlung schon über 0,5“); Ende Februar = Annahme |
|
||
| Puderzucker (Milben je 100 Bienen, %) | Mai–Juli | unter 1 % | 1–5 % | über 5 % | LWG „Juli“; Mai und Juni = Annahme |
|
||
| Puderzucker | August–Oktober | unter 2 % | 2–5 % | über 5 % | LWG „September“; August und Oktober = Annahme |
|
||
| Puderzucker | November–April | keine Ampel | | | – |
|
||
| Auswaschung | wie Puderzucker | | | | Annahme (gleiche Befallsrate) |
|
||
| Drohnenbrut | keine Ampel | | | | – |
|
||
|
||
- **Grenzen:** rot heißt `Wert > redAbove`. Gelb heißt `Wert ≥ yellowFrom`. Genau 10,0 Milben/Tag sind also gelb. Heute (T3) ist 10,0 rot; das ändert sich mit der Quelle.
|
||
- **Jungvölker** (`YOUNG_COLONY`, `NUCLEUS_COLONY`, `MATING_NUCLEUS`): Schwellen des natürlichen Milbenfalls × 0,5 (Fahrplan §10). Nur beim Milbenfall, weil Prozentwerte schon auf die Probe bezogen sind (Annahme).
|
||
- **Nach einer Behandlung:** Liegt das Messdatum bis 14 Tage nach `endDate ?? date` einer Behandlung, gibt es keine Ampel. Der Text lautet „Behandlungseffekt – Nachkontrolle ab {Datum}“. Die Regel schlägt dann nichts vor (Fahrplan §10).
|
||
- **Wirkung:** Rot → PLAN-Vorschlag `VARROA_TREAT` „Behandlung prüfen“, Priorität `HIGH`, Anker = Mess-ID.
|
||
- **Config:** `{ naturalFall: { seasonMonths, yellowFrom: 5, redAbove: 10, winterRedAbove: 0.5 }, infestationPercent: { summerMonths: [5,6,7], summer: { yellowFrom: 1, redAbove: 5 }, autumnMonths: [8,9,10], autumn: { yellowFrom: 2, redAbove: 5 } }, youngColonyFactor: 0.5, youngColonyTypes, recheckAfterDays: 14, notify: true }`.
|
||
- Ein Helfer `varroa-bands.ts` (`bandsFor(date, colonyType, config)`, `classify(value, band)`) liefert Ampel, Endpunkt und Regel. Das Frontend klassifiziert nur mit dem gelieferten Band.
|
||
|
||
### 7.2 Fälligkeit der Durchsicht
|
||
|
||
`inspectionDueOn(colony, lastInspection, segment, config)` = Datum der letzten Durchsicht (sonst `colony.createdAt`) plus Intervall des heutigen Abschnitts. In `WINTERRUHE` `null`.
|
||
|
||
| Abschnitt | Intervall (Standard) | erlaubt | Quelle |
|
||
|---|---|---|---|
|
||
| `FRUEHJAHR` | 14 Tage | 7–35 | Fahrplan §10 („sonst 14–21“) |
|
||
| `SCHWARMZEIT` | 7 Tage | 7–9 | Fahrplan §10; Bieneninstitute (z. B. LLH Kirchhain, LWG): Weiselzellen sind nach 8–9 Tagen verdeckelt, darum Kontrolle im 7-Tage-Takt |
|
||
| `SOMMER` | 14 Tage | 7–35 | Fahrplan §10 |
|
||
| `SPAETSOMMER` | 21 Tage | 7–35 | Fahrplan §10 |
|
||
| `HERBST` | 21 Tage | 7–35 | Fahrplan §10 |
|
||
| `WINTERRUHE` | keins | – | Winterruhe; Einwinterung und Auswinterung laufen über Regeln A9/A10 |
|
||
|
||
- Erste Durchsicht im Jahr: Liegt die letzte Durchsicht vor dem Beginn von `FRUEHJAHR`, ist das Volk ab Abschnittsbeginn fällig. Die Begründung sagt „Erste Durchsicht im Jahr“.
|
||
- Ausgabe: `inspectionDueOn` in `ColonyOverviewRow` (T3), in `GET /colonies/:id/next-steps` und in `GET /autopilot/today`. Das beantwortet §30 „Welche Völker wurden zu lange nicht kontrolliert?“.
|
||
|
||
### 7.3 Pflanzenentwicklung und Saisonabschnitt
|
||
|
||
**Grünlandtemperatursumme (DWD):** Summe der positiven Tagesmitteltemperaturen ab 1. Januar. Januar zählt mit Faktor 0,5, Februar mit 0,75, ab März voll. Ab 200 °C gilt die Vegetation als nachhaltig begonnen. Fehlen mehr als 10 % der Tage, gilt die Summe als lückenhaft und wird nicht benutzt (Text „Wetterdaten lückenhaft“).
|
||
|
||
**Phasen:**
|
||
|
||
| Phase | Meldung (ein Tipp) | Bezug | GTS-Schwelle | Kalender-Rückfall |
|
||
|---|---|---|---|---|
|
||
| `SALWEIDE` | „Salweide blüht“ | Imkerpraxis: erste Pollentracht, Frühjahrsdurchsicht | 200 °C (DWD-Vegetationsbeginn; gleichgesetzt mit der Salweidenblüte = Annahme) | 20.03. |
|
||
| `KIRSCHE` | „Kirsche blüht“ | Imkerpraxis: Honigraum, Beginn der Schwarmkontrollen | aus eigenen Meldungen der Vorjahre, sonst keine | 15.04. |
|
||
| `APFEL` | „Apfel blüht“ | DWD-Vollfrühling | aus eigenen Meldungen der Vorjahre, sonst keine | 28.04. |
|
||
| `HOLUNDER` | „Holunder blüht“ | DWD-Frühsommer | keine | 30.05. |
|
||
| `LINDE` | „Linde blüht“ | DWD-Hochsommer | keine | 20.06. |
|
||
| `TRACHTENDE` | „Tracht vorbei“ | Imkerpraxis: letzte Ernte | keine | 01.08. |
|
||
| `HERBST` | „Efeu blüht“ | Imkerpraxis: letzte Tracht | keine | 15.09. |
|
||
| `WINTER` | „Winterruhe“ | brutarm bis brutfrei | keine | 15.11. |
|
||
|
||
Alle Kalender-Rückfälle sind Annahmen für Schwaben (rund 450–500 m). Vor dem Bau gegen DWD-Phänologiedaten der Region prüfen.
|
||
|
||
**Effektive Phase** je Stand und Jahr, als Maximum dreier Werte:
|
||
1. **Gemeldet:** höchste Phase aller nicht zurückgezogenen Meldungen dieses Jahres für den Stand oder für alle Stände.
|
||
2. **Geschätzt (GTS):** höchste Phase mit Schwelle ≤ heutiger GTS, nur wenn die GTS nicht lückenhaft ist.
|
||
3. **Kalender:** höchste Phase mit Rückfall-Datum ≤ heute, nur für Phasen ohne GTS-Schwelle oder ohne Wetterdaten am Stand.
|
||
|
||
`phaseSource` nennt die Quelle des Maximums. Innerhalb eines Jahres läuft die Phase nur vorwärts. Einzige Ausnahme: Eine zurückgezogene Meldung kann sie zurücksetzen.
|
||
|
||
**Kalibrierung:** Für `KIRSCHE` und `APFEL` (und auf Wunsch `SALWEIDE`) nimmt der Job als Schwelle den Median der GTS an den Tagen der ersten eigenen Meldung dieser Phase in bis zu drei Vorjahren. Ohne Vorjahresmeldung gibt es keine Schwelle; dann gilt der Kalender. So erfindet ApiCore keine Schwellen.
|
||
|
||
**Saisonabschnitte:**
|
||
|
||
| Abschnitt | ab Phase | bis vor Phase |
|
||
|---|---|---|
|
||
| `WINTERRUHE` | `WINTER` oder vor `SALWEIDE` | `SALWEIDE` |
|
||
| `FRUEHJAHR` | `SALWEIDE` | Beginn Schwarmzeit |
|
||
| `SCHWARMZEIT` | `KIRSCHE` (einstellbar: `APFEL`) | `LINDE` (einstellbar) |
|
||
| `SOMMER` | `LINDE` | `TRACHTENDE` |
|
||
| `SPAETSOMMER` | `TRACHTENDE` | `HERBST` |
|
||
| `HERBST` | `HERBST` | `WINTER` |
|
||
|
||
Beginn der Schwarmzeit ab Kirschblüte ist eine Annahme nach Bieneninstitut-Praxis. Früher prüfen kostet eine Durchsicht, ein verpasster Schwarm kostet die Ernte eines Volks.
|
||
|
||
**Config `PHENOLOGY`:** `{ useGts: true, gtsThresholds: { SALWEIDE: 200 }, calibrateFromReports: true, calendarFallback: { SALWEIDE: '03-20', … }, swarmSeason: { from: 'KIRSCHE', until: 'LINDE' }, maxGapShare: 0.1 }`.
|
||
|
||
Stände ohne Koordinaten: nur Meldungen und Kalender. Die Begründung sagt „Ohne Koordinaten keine Schätzung“.
|
||
|
||
### 7.4 Biotechnik-Ketten
|
||
|
||
| Auslöser | Schritt (Vorschlag, Art) | fällig ab Auslöser-Datum | Quelle |
|
||
|---|---|---|---|
|
||
| Durchsicht mit `queenCellsPresent = true` | „Schwarmkontrolle“ (PLAN) | +7 Tage | Fahrplan §10; Bieneninstitute (Verdeckelung nach 8–9 Tagen) |
|
||
| `QueenAssigned` | „Annahme der Königin prüfen“ (PLAN) | +7 Tage | Fahrplan §10 |
|
||
| Ableger `RAISE_OWN` | „Ableger-Plan übernehmen“ (PLAN, drei Termine) | +9 „Nachschaffungszellen prüfen“; +21 „Begattung prüfen“ (Fenster bis +24); +28 „Stifte prüfen“ | Fahrplan §10 |
|
||
| Ableger `QUEEN_CELL` | „Stifte prüfen“ (PLAN) | +21 Tage | Annahme |
|
||
| Ableger `QUEEN` | „Annahme der Königin prüfen“ (PLAN) | +7 Tage | wie Königin zugesetzt |
|
||
| Ableger ohne Angabe | keine Kette | – | keine stille Annahme |
|
||
|
||
- Ein Ableger-Plan ist **ein** Vorschlag. „Übernehmen“ legt drei Aufgaben mit Terminen an. Eine Entscheidung, drei Kalendereinträge.
|
||
- Dubletten: Ein offener Vorschlag oder eine offene Aufgabe mit derselben Absicht für dasselbe Volk verhindert einen zweiten. Ein Ableger mit `QUEEN` plus spätere Zuweisung der Königin ergibt also nur einen Annahme-Vorschlag.
|
||
- Fälligkeit zählt ab dem fachlichen Datum (Durchsicht, Zuweisung, Ableger), nicht ab heute.
|
||
|
||
### 7.5 Wartezeit-Kette
|
||
|
||
- Ein Helfer `withdrawal-window.ts`: Fenster einer Behandlung = `(endDate ?? date)` bis `(endDate ?? date) + withdrawalPeriodDays`. Nutzer: `getWithdrawalStatus`, `withdrawalEndDay` (Rückgrat), `WithdrawalCheckService`, Kalender.
|
||
- `WithdrawalCheckService.conflictsForHarvests(db, harvestIds)`: je Ernte und Volk die Behandlung, deren Fenster das **Erntedatum** einschließt (Annahme 10). Ohne eingetragene Wartezeit keine Warnung (Annahme 10).
|
||
- **Ernte, Schleuderung, Abfüllung:** Die Antwort trägt `warnings` mit Code `WITHDRAWAL_ACTIVE` (Volk, Mittel, „bis“). Nie 4xx.
|
||
- **Meldung:** ein Handler je Stufe. Schlüssel `withdrawal-conflict:<harvestId>:<colonyId>:<userId>`. Jeder Konflikt wird nur einmal gemeldet, egal auf welcher Stufe er zuerst auffällt.
|
||
- **„Volk erntefähig“:** `WithdrawalEnded` → INFO „{Volk} ist wieder erntefähig. Wartezeit nach {Mittel} endete am {Datum}.“ an Inhaber von `harvests:create`. Nur, wenn für das Volk kein weiteres Fenster läuft.
|
||
- **Kalender:** Quelle `WITHDRAWAL` je Behandlung mit Wartezeit, ganztägig vom Fensterbeginn bis zum Ende, Titel „Wartezeit {Volk} · {Mittel}“. In-App-Kalender: Typ `WITHDRAWAL` mit `endDate`. Ohne Wartezeit wird die Quelle abgemeldet (Graph-Event gelöscht, Rückgrat Task 8).
|
||
|
||
### 7.6 Honig-Kette: Vorbelegung und Plausibilität
|
||
|
||
Vorbelegung ist kein Messwert. Was eine Messung ist (Menge, Wassergehalt), erscheint als Hinweis mit Übernahme-Knopf, nicht still im Feld. Dafür wird `PreviousValueHint` (T3) um ein Präfix erweitert: „Aus 2 Ernten: 42,5 kg · übernehmen“.
|
||
|
||
| Formular | Vorschlag | Herkunft |
|
||
|---|---|---|
|
||
| Schleuderung | Menge = Σ `netWeight` der gewählten Ernten | `GET /extractions/prefill` |
|
||
| Schleuderung | Sorte, wenn alle Ernten dieselbe `honeyType` haben | dto |
|
||
| Honigcharge | Menge = Schleudermenge − Σ `initialAmount` vorhandener Chargen der Schleuderung | `GET /honey-batches/prefill` |
|
||
| Honigcharge | Sorte, Wassergehalt der Schleuderung | dto |
|
||
|
||
| Prüfung | Code | Stufe |
|
||
|---|---|---|
|
||
| Schleudermenge > Σ Netto der Ernten (alle Netto bekannt) | `EXTRACTION_EXCEEDS_HARVEST` | WARNUNG |
|
||
| Netto fehlt bei mindestens einer Ernte | `HARVEST_NET_MISSING` | HINWEIS |
|
||
| Σ Ausgangsmenge der Chargen > Schleudermenge | `BATCHES_EXCEED_EXTRACTION` | WARNUNG |
|
||
|
||
Geprüft wird beim Anlegen und Ändern. Antwort `201/200` mit `warnings`, nie eine Sperre.
|
||
|
||
### 7.7 Bewegungsbuch der Honigcharge
|
||
|
||
`HoneyBatchLedgerService.bookIn(tx, { honeyBatchId, deltaKg, type, reason?, notes?, sourceBottlingBatchId?, actingUserId })`:
|
||
|
||
1. `SELECT … FROM "HoneyBatch" WHERE id = $1 FOR UPDATE`.
|
||
2. Bei negativem Delta: Rest ≥ |Delta| − 0,5 g, sonst 400 „Abfüllmenge übersteigt die verfügbare Menge der Honigcharge“ (Text wie heute).
|
||
3. `BatchMovement` in Gramm schreiben, `amount` auf Gramm gerundet setzen, nie unter 0.
|
||
|
||
| Vorgang | Buchung |
|
||
|---|---|
|
||
| Charge anlegen mit Menge | `initialAmount = amount`, keine Buchung |
|
||
| Abfüllung anlegen | `BOTTLING`, −Menge, mit `sourceBottlingBatchId` |
|
||
| Abfüllung ändern | `BOTTLING`, Differenz |
|
||
| Abfüllung entfernen | `BOTTLING`, +Menge, Notiz „Abfüllung entfernt“ |
|
||
| `PATCH /honey-batches/:id` mit `amount` | `CORRECTION`, Differenz neu − aktuell |
|
||
| Entnahme (lose Ware, Probe, Bruch) | `WITHDRAWAL` mit `WithdrawalReason` |
|
||
| `amount: null` bei vorhandenen Buchungen | 400 „Die Menge wird über das Bewegungsbuch geführt. Bitte eine Korrektur buchen.“ |
|
||
| `amount` von `null` auf einen Wert | setzt `initialAmount`, keine Buchung |
|
||
|
||
`takeFromHoneyBatch`, `giveBackToHoneyBatch` und `normalizeAmount` in `bottling.service.ts` entfallen. Ein Wächter verbietet Schreibzugriffe auf `honeyBatch.amount` außerhalb des Ledgers.
|
||
|
||
### 7.8 Qualität vor der Abfüllung
|
||
|
||
| Grenze | Standard | Quelle | Wirkung |
|
||
|---|---|---|---|
|
||
| Warnung | über 18,0 % Wasser | D.I.B., Warenzeichen „Echter Deutscher Honig“ | `WATER_CONTENT_HIGH`, WARNUNG |
|
||
| Bestätigung | über 20,0 % Wasser | HonigV Anlage 2 (RL 2001/110/EG Anhang II): im Allgemeinen höchstens 20 % | `WATER_CONTENT_ABOVE_LEGAL`, Abfüllen nur mit Bestätigung |
|
||
| Heidekrauthonig | Bestätigung über 23 % | HonigV Anlage 2 | nur, wenn die Sorte in der Config so heißt; keine Vorbelegung |
|
||
| kein Messwert | – | – | `WATER_CONTENT_MISSING`, HINWEIS |
|
||
|
||
- Maßgeblich ist die jüngste `HoneyMeasurement` (nach `measuredAt`), sonst `HoneyBatch.waterContent`.
|
||
- Sorten-Abgleich: `honeyType` ohne Groß-/Kleinschreibung, exakt. Config `{ warnAbovePercent: 18, confirmAbovePercent: 20, byHoneyType: {} }`.
|
||
- Dieselbe Prüfung meldet sich auch beim Erfassen einer Honigmessung (nur Warnung).
|
||
- **Bestätigung:** `POST /bottling` ohne `acknowledgedWarnings: ['WATER_CONTENT_ABOVE_LEGAL']` → 409 `{ code: 'CONFIRMATION_REQUIRED', warnings }`. Der Idempotenz-Interceptor gibt den Schlüssel bei einem Fehler frei; die bestätigte Wiederholung darf denselben Schlüssel nutzen.
|
||
- **MHD und Glas:** Mit gewähltem Artikel sind `jarSize` und `bestBeforeDate` optional. Der Server nimmt `product.packagingSize` und `date + shelfLifeMonths` (Monatsende abgeschnitten, z. B. 31.01. + 1 Monat = 28./29.02.). Ein gesendeter Wert gewinnt. Weicht `jarSize` vom Artikel ab: `JAR_SIZE_DIFFERS`, HINWEIS. Für das MHD gibt es keinen Standardwert; der Imker trägt die Haltbarkeit am Artikel ein.
|
||
- Das Formular füllt Glasgröße und MHD direkt aus dem Artikel (Stammdaten, keine Beobachtung) und nennt die Herkunft: „aus Artikel: 24 Monate“.
|
||
|
||
### 7.9 Durchsicht → Volkskarte
|
||
|
||
`CreateInspectionDto.applyToColony?: ('occupiedFrames' | 'broodStatus' | 'feedStatus')[]` (`@ArrayUnique`, höchstens 3).
|
||
|
||
| Feld der Volkskarte | aus der Durchsicht | Abbildung |
|
||
|---|---|---|
|
||
| `occupiedFrames` | `colonyStrength` (besetzte Waben) | gleich, 0–40 (4.14). Der Altwert `strength` wird nie geschrieben. |
|
||
| `broodStatus` | `broodPattern` (1–5, 5 = gut) | 5 `EXCELLENT`, 4 `GOOD`, 3 `AVERAGE`, 2 `WEAK`, 1 `CRITICAL` (Brutbild als Brutstatus = Annahme) |
|
||
| `feedStatus` | `feedStatus` | gleich (4.4) |
|
||
|
||
- Der Server nimmt die Werte aus der Durchsicht selbst, nie aus dem Request. Es gibt keine zweite Wahrheit.
|
||
- In derselben Transaktion: Durchsicht, Audit, `InspectionRecorded`, dann `ColoniesService.updateIn` (eigener Audit-Eintrag `Colony UPDATE`).
|
||
- Übersprungen und gemeldet in `colonyTakeover.skipped` wird ein Feld bei:
|
||
- `NO_VALUE`: Feld in der Durchsicht leer.
|
||
- `NO_PERMISSION`: kein `colonies:update`. Kein 403, damit die Offline-Warteschlange nicht hängen bleibt.
|
||
- `NEWER_INSPECTION`: Es gibt eine neuere Durchsicht des Volks (nach `date`), z. B. bei einem spät gesendeten Offline-Eintrag.
|
||
- `OUT_OF_RANGE`: Wert passt nicht zur Volkskarte.
|
||
- Antwort: `{ …inspection, colonyTakeover: { applied: Field[], skipped: { field, reason }[] } }`.
|
||
- Beim Bearbeiten einer Durchsicht wird nichts übernommen. Eine Korrektur löst keine Regel aus (wie Rückgrat Task 11).
|
||
- Das Formular zeigt den Abschnitt „Volkskarte“ direkt über „Speichern“: je Feld eine Zeile „Futter: Ausreichend → Niedrig“ mit Haken. Angehakt sind nur Felder, die in dieser Durchsicht ausgefüllt wurden. Ohne `colonies:update` fehlt der Abschnitt.
|
||
|
||
### 7.10 Volk beendet
|
||
|
||
1. **In der Transaktion des Endes:** `releaseQueenIn` (5.4). Das gilt auch beim Verlust über die Auswinterung und bei `update` auf `SOLD`/`DECEASED`. Der Status der Königin ändert sich hier nicht.
|
||
2. **Handler `suggestions.colony-ended`:**
|
||
- offene Vorschläge des Volks → `OBSOLETE`;
|
||
- gibt es offene Aufgaben des Volks oder eine Königin mit Status `ACTIVE`, die eben freigegeben wurde: ein Vorschlag `CLEANUP` (DO), Priorität `NORMAL`, Begründung „B3 ist eingegangen (Auswinterung 12.03.). 2 Aufgaben sind noch offen.“
|
||
3. **„Aufräumen“** öffnet ein Sheet mit Vorauswahl, alles sichtbar und änderbar:
|
||
- Aufgaben: „Abbrechen“ (`CANCELLED`) vorgewählt; je Aufgabe umschaltbar auf „Erledigt“ oder „Behalten“.
|
||
- Königin: bei `DECEASED` „verstorben“, bei `SOLD` „verkauft“ vorgewählt; bei `DISSOLVED`/`MERGED` „unverändert“ (Annahme).
|
||
- Ein Hauptknopf „Aufräumen“.
|
||
|
||
## 8. Saison-Autopilot
|
||
|
||
### 8.1 Eingaben (je lebendem Volk)
|
||
|
||
- Volk: Status, Typ, Volkskarte, Stand, `createdAt`.
|
||
- Letzte Durchsicht: Datum, `colonyStrength`, `feedStatus`, `queenCellsPresent`.
|
||
- Letzte Varroa-Messung und ihr Band (7.1); letzte Behandlung und ihr Fenster.
|
||
- Fütterungen seit Beginn von `SPAETSOMMER` bzw. seit der letzten Durchsicht.
|
||
- Königin: `hatchYear`. Überwinterungseinträge (`WinterCheck`) des laufenden und des letzten Winters.
|
||
- Offene Aufgaben (`relatedType = COLONY`) und offene Vorschläge des Volks.
|
||
- `ApiaryPhenology` des Stands: Phase, Abschnitt, Quelle, Datum der Meldung.
|
||
- Heute (Europe/Berlin) und die Config von `SEASON_AUTOPILOT`, `INSPECTION_INTERVAL`, `PHENOLOGY`.
|
||
|
||
Keine Eingabe aus Wetter-Prognosen, keine Waage, kein ML.
|
||
|
||
### 8.2 Regeln
|
||
|
||
Ausgabe jeder Regel ist ein Kandidat für einen Vorschlag mit `source = AUTOPILOT`, `ruleKey = SEASON_AUTOPILOT` (A1: `INSPECTION_INTERVAL`).
|
||
|
||
| Nr | Absicht | Wann (alle Bedingungen) | Schritt, Art | Begründung (Beispiel) | Anker | Quelle |
|
||
|---|---|---|---|---|---|---|
|
||
| A1 | `INSPECT` | Abschnitt ≠ `WINTERRUHE`; `inspectionDueOn` ≤ heute + 2 | „Durchsicht“, DO | „Letzte Durchsicht vor 8 Tagen. In der Schwarmzeit alle 7 Tage.“ | ID der letzten Durchsicht oder `none` | 7.2 |
|
||
| A2 | `VARROA_MEASURE` | Abschnitt ≠ `WINTERRUHE` und letzte Messung älter als 28 Tage; oder `WINTERRUHE`, 15.11.–20.12., keine Messung seit 01.11. | „Varroa messen“, DO | „Letzte Messung vor 35 Tagen.“ / „Vor der Winterbehandlung den Milbenfall messen.“ | Mess-ID bzw. `winter-<Jahr>` | 28 Tage aus dem Rückgrat; Winterfenster = Annahme nach LWG-Konzept (Restentmilbung im brutfreien Volk) |
|
||
| A3 | `VARROA_RECHECK` | Behandlungsende vor ≥ 14 Tagen, keine Messung seither | „Behandlungserfolg messen“, DO | „Behandlung mit Oxuvar endete am 02.08. Nachkontrolle ab 16.08.“ | Behandlungs-ID | Fahrplan §10 (LWG) |
|
||
| A4 | `SUMMER_TREATMENT` | Abschnitt `SPAETSOMMER`; keine Behandlung seit Abschnittsbeginn | „Sommerbehandlung planen“, PLAN | „Tracht vorbei (gemeldet am 25.07.). Letzte Ernte am 20.07.“ | Jahr | LWG-Varroakonzept: Behandlung nach der letzten Ernte |
|
||
| A5 | `FEED` | Abschnitt `SPAETSOMMER` oder `HERBST`; keine Fütterung seit Beginn `SPAETSOMMER` | „Einfüttern“, DO | „Tracht vorbei. Noch keine Fütterung erfasst.“ | Jahr | Imkerpraxis; bewusst ohne Zielmenge |
|
||
| A6 | `FEED` | Abschnitt ≠ `WINTERRUHE`; Futter der letzten Durchsicht `WEAK`/`CRITICAL` oder Volkskarte `CRITICAL`; keine Fütterung seither | „Füttern“, DO | „Futter bei der Durchsicht am 03.04.: niedrig.“ | Durchsicht-ID | Lastenheft §23 |
|
||
| A7 | `HONEY_SUPER` | Phase ≥ `KIRSCHE` und < `LINDE`; `colonyStrength` der letzten Durchsicht ≥ 8 | „Honigraum prüfen“, DO | „Kirsche blüht (geschätzt). B3 besetzt 9 Waben.“ | Jahr | Imkerpraxis; 8 Waben = Annahme (`honeySuperMinFrames`) |
|
||
| A8 | `QUEEN_REPLACE` | Abschnitt `SCHWARMZEIT` oder `SOMMER`; `hatchYear` ≤ Jahr − 2 | „Umweiseln planen“, PLAN | „Königin Q-12 ist von 2024.“ | Königin-ID und Jahr | Imkerpraxis; 2 Jahre = Annahme (`queenMaxAgeYears`) |
|
||
| A9 | `WINTER_CHECK` | Abschnitt `HERBST`; keine Einwinterung für den Winter dieses Jahres | „Einwinterung erfassen“, DO | „Für den Winter 2026/27 fehlt die Einwinterung.“ | `<winterYear>-AUTUMN` | Annahme 16 |
|
||
| A10 | `WINTER_CHECK` | Abschnitt `FRUEHJAHR`; Einwinterung des Vorwinters da, Auswinterung fehlt | „Auswinterung erfassen“, DO | „Salweide blüht. Auswinterung 2025/26 fehlt.“ | `<winterYear>-SPRING` | Annahme 16 |
|
||
|
||
- `VARROA_TREAT` entsteht nur aus der Varroa-Regel (7.1), nicht aus dem Autopilot.
|
||
- Fälligkeit: A1 `inspectionDueOn`; A2/A3 der Tag, ab dem die Bedingung gilt; sonst heute.
|
||
- Priorität: A1 überfällig `HIGH`, sonst `NORMAL`; A4 `HIGH`; übrige `NORMAL`.
|
||
- Ablauf: `expiresAt` = Fälligkeit + 30 Tage (`suggestionTtlDays`, Annahme für eine kurze Liste).
|
||
- Config `SEASON_AUTOPILOT`: `{ honeySuperMinFrames: 8, varroaMeasureIntervalDays: 28, winterVarroaWindow: { from: '11-15', to: '12-20' }, queenMaxAgeYears: 2, suggestionTtlDays: 30, maxStepsPerColony: 3, rules: { A1: true, …, A10: true } }`. Jede Regel einzeln abschaltbar.
|
||
|
||
### 8.3 Abgleich (`AutopilotService.syncColonyIn(tx, colonyId, today)`)
|
||
|
||
1. Volkszeile sperren. Volk beendet → Ende.
|
||
2. Eingaben über `tx` laden (eine feste Folge von Abfragen, keine N+1).
|
||
3. `evaluateAutopilot(input, config, today)` ist eine reine Funktion. Sie liefert Kandidaten `{ intent, mode, reasonCode, evidence, dueDate, priority, anchor }`.
|
||
4. Kandidat verwerfen, wenn für das Volk schon eine offene Aufgabe oder ein offener Vorschlag derselben Absicht besteht.
|
||
5. Neue Kandidaten anlegen (`skipDuplicates` über `dedupeKey`). Offene Autopilot-Vorschläge mit gleichem Schlüssel bekommen aktuelle Belege, Fälligkeit und Priorität.
|
||
6. Offene Autopilot-Vorschläge des Volks, deren Schlüssel nicht mehr vorkommt → `RESOLVED` („Bedingung entfällt“). Beispiel: Die Durchsicht ist erfasst, also verschwindet „Durchsicht fällig“.
|
||
7. Regel- und Kettenvorschläge fasst der Abgleich nicht an.
|
||
8. Ist `SEASON_AUTOPILOT` aus, werden alle offenen Autopilot-Vorschläge `RESOLVED` („Autopilot aus“).
|
||
|
||
### 8.4 Rangfolge (deterministisch)
|
||
|
||
Schritte eines Volks = offene Aufgaben (`OPEN`, `IN_PROGRESS`) + offene Vorschläge, nach Leserechten gefiltert. Sortiert nach diesem Tupel:
|
||
|
||
1. Dringlichkeit: `IN_PROGRESS` oder überfällig oder heute = 0; in ≤ 3 Tagen = 1; in ≤ 14 Tagen = 2; später = 3; ohne Datum = 4. Ohne Datum, aber `URGENT` = 0.
|
||
2. Priorität: `URGENT`, `HIGH`, `NORMAL`, `LOW`.
|
||
3. Absicht (feste Tabelle): `VARROA_TREAT`, `QUEEN_CHECK`, `QUEEN_ACCEPTANCE`, `SWARM_CHECK`, `SPLIT_PLAN`, `FEED`, `INSPECT`, `VARROA_RECHECK`, `HEALTH_CHECK`, `SUMMER_TREATMENT`, `VARROA_MEASURE`, `WINTER_CHECK`, `HONEY_SUPER`, `QUEEN_REPLACE`, `FOLLOW_UP`, `CLEANUP`, `STOCK_REORDER`. Eine Aufgabe ohne Vorschlag zählt wie `FOLLOW_UP`.
|
||
4. Fälligkeit aufsteigend, dann `createdAt`, dann `id`.
|
||
|
||
**Zusammenlegen:** Schritte, die dasselbe Formular öffnen (z. B. `INSPECT` und `SWARM_CHECK` → Durchsicht), werden ein Schritt. Er erbt den dringlichsten Rang und zeigt höchstens zwei Gründe: „Durchsicht mit Schwarmkontrolle. Weiselzellen am 05.05. gesehen. In der Schwarmzeit alle 7 Tage.“
|
||
|
||
### 8.5 Ausgabe „die nächsten 3 Schritte“
|
||
|
||
```ts
|
||
interface NextStep {
|
||
key: string; // 'task:<id>' | 'suggestion:<id>'
|
||
kind: 'TASK' | 'SUGGESTION';
|
||
intent: SuggestionIntent | null;
|
||
title: string; // „Durchsicht mit Schwarmkontrolle“
|
||
reasons: string[]; // höchstens 2 kurze Sätze
|
||
dueOn: string | null; // YYYY-MM-DD
|
||
urgency: 'OVERDUE' | 'TODAY' | 'SOON' | 'LATER' | 'NONE';
|
||
action:
|
||
| { type: 'CAPTURE'; form: 'inspection' | 'varroa' | 'feeding' | 'treatment' | 'winter-check'; href: string }
|
||
| { type: 'ACCEPT'; suggestionId: string }
|
||
| { type: 'CLEANUP'; suggestionId: string }
|
||
| { type: 'OPEN_TASK'; taskId: string; href: string };
|
||
dismissible: boolean;
|
||
}
|
||
interface ColonyNextSteps {
|
||
colonyId: string; segment: SeasonSegment; phase: PhenologyPhase | null; phaseSource: PhaseSource | null;
|
||
inspectionDueOn: string | null; withdrawalUntil: string | null; steps: NextStep[]; // höchstens 3
|
||
}
|
||
```
|
||
|
||
Beispiel für B3 am 12.05.: (1) „Durchsicht mit Schwarmkontrolle“ – überfällig seit 1 Tag, „Weiselzellen am 05.05. gesehen.“ – Erfassen; (2) „Behandlung prüfen“ – „12,4 Milben pro Tag am 10.05. Grenze: 10 (LWG).“ – Übernehmen; (3) „Honigraum prüfen“ – „Kirsche blüht (gemeldet am 18.04.). B3 besetzt 9 Waben.“ – Erfassen.
|
||
|
||
`withdrawalUntil` erscheint als Hinweis „Wartezeit bis 20.08.“, nicht als Schritt.
|
||
|
||
### 8.6 Was der Autopilot nicht tut
|
||
|
||
- Er legt keine Aufgaben an. Nur „Übernehmen“ tut das.
|
||
- Er ändert keine Fachdaten. Er schreibt nur Vorschläge.
|
||
- Er rät keine Mengen (Futter, Ernte) und keine Diagnosen.
|
||
- Er nutzt keine Prognosen, nur Erfasstes, Gemeldetes und gemessenes Wetter.
|
||
|
||
## 9. Vorschläge
|
||
|
||
### 9.1 Lebenszyklus
|
||
|
||
| von | nach | Auslöser |
|
||
|---|---|---|
|
||
| – | `OPEN` | Handler oder Job (`proposeIn`) |
|
||
| `OPEN` | `ACCEPTED` | „Übernehmen“ (PLAN); legt Aufgabe(n) über `TasksService.createInTx` an |
|
||
| `OPEN` | `DISMISSED` | „Verwerfen“ |
|
||
| `DISMISSED` | `OPEN` | „Rückgängig“ oder „Wieder öffnen“ (bis 30 Tage) |
|
||
| `OPEN` | `RESOLVED` | Abgleich: Bedingung entfällt (DO); Aufräumen ausgeführt (CLEANUP) |
|
||
| `OPEN` | `EXPIRED` | Job, `expiresAt` überschritten |
|
||
| `OPEN` | `OBSOLETE` | Volk beendet |
|
||
|
||
- Jeder Übergang ist ein Compare-and-Set (`UPDATE … WHERE status = 'OPEN'`), mit Audit-Eintrag (`entityType: 'Suggestion'`).
|
||
- Ein DO-Vorschlag hat im Menü „Als Aufgabe planen“. Das ist „Übernehmen“ mit Datum.
|
||
|
||
### 9.2 Dubletten und Anker
|
||
|
||
- Je Regel und Objekt gibt es höchstens eine offene Sache: Besteht schon ein offener Vorschlag oder eine offene Aufgabe, entsteht kein neuer Vorschlag. Das hält Annahme 12. Die Prüfung läuft unter der Zeilensperre der Regel wie in `triggerActionIn` und zusätzlich über `dedupeKey`.
|
||
- Ein verworfener Vorschlag kommt für denselben Anker nie wieder. Ein neuer Anker (neue Messung, neue Durchsicht, neues Jahr) ergibt einen neuen Vorschlag.
|
||
- Regel-Vorschläge: Anker = auslösender Datensatz. Zeitbasierte: Anker = Tag oder Jahr.
|
||
|
||
### 9.3 Texte
|
||
|
||
- `src/suggestions/suggestion-texts.ts`: je `reasonCode` eine Vorlage `(evidence, locale de-DE) → { title, reasons }`. Zahlen und Daten deutsch formatiert.
|
||
- Höchstens zwei Sätze, je Satz höchstens 90 Zeichen. Ein Wächter prüft jede Vorlage mit Beispielbelegen.
|
||
- `actionTemplate` einer Regel überschreibt nur den Titel, wie heute.
|
||
|
||
### 9.4 Meldungen zu Vorschlägen
|
||
|
||
- Nur Regeln mit `config.notify = true` schreiben zusätzlich eine Meldung (WARNUNG, bei `URGENT` KRITISCH) an Inhaber von `tasks:read` mit dem Leserecht des Vorschlags.
|
||
- Alle anderen Vorschläge stehen nur in der Liste. Weniger Lärm.
|
||
|
||
### 9.5 Bestehende Automatik-Aufgaben
|
||
|
||
- Offene Aufgaben mit `sourceAutomationTrigger` bleiben Aufgaben. Sie gelten als übernommen.
|
||
- Das Abzeichen `TaskSourceBadge` heißt künftig „Aus Vorschlag“ (statt „Vorschlag“). Echte Vorschläge sind keine Aufgaben mehr.
|
||
|
||
## 9a. Erinnerungen und Telegram (Inhaber-Antwort 3)
|
||
|
||
### 9a.1 Überblick
|
||
|
||
```
|
||
daily-evaluation ──TaskDue──▶ Handler notifications.task-due ──┐
|
||
daily-evaluation Schritt 5 (digest) ───────────────────────────┤
|
||
alle anderen Meldungen (recordForPermission) ──────────────────┤
|
||
▼
|
||
Notification (je Nutzer) + NotificationDelivery (TELEGRAM, PENDING)
|
||
│ gleiche Transaktion, pg_notify('notification_delivery','')
|
||
▼
|
||
Worker-Schleife telegram-send ── sendMessage ──▶ Telegram Bot API
|
||
Worker-Schleife telegram-updates ◀── getUpdates (Long Polling) ── /start <token>, /stop, /hilfe
|
||
```
|
||
|
||
- Handler und Jobs schreiben nur Zeilen. Netz gibt es nur in den zwei Worker-Schleifen (Rückgrat-Handler-Vertrag).
|
||
- `OutboxTable` bekommt `'NotificationDelivery'`. Claim, Abschluss, Freigabe und Wiederholung kommen unverändert aus `outbox-claims.ts` und `retry-policy.ts`. `WAKE_CHANNELS` bekommt `notificationDeliveries: 'notification_delivery'`.
|
||
- Ohne `TELEGRAM_BOT_TOKEN` laufen beide Schleifen nicht. Meldungen in der App gibt es trotzdem.
|
||
|
||
### 9a.2 Meldung je fälliger Aufgabe
|
||
|
||
- Auslöser: `TaskDue` (Rückgrat, einmal am Fälligkeitstag, mit Nachholfenster).
|
||
- Empfänger: der Zuständige (`assignedUserId`); ohne Zuständigen alle aktiven Nutzer mit `tasks:complete`. Nur, wer `taskDueEnabled` hat und `tasks:read` hält.
|
||
- Text: „Heute fällig: {Titel}“, Link `/tasks`. Kategorie `TASK_DUE`, `requiredPermissions: ['tasks:read']`.
|
||
- Telegram-Zeitpunkt: frühestens eine Minute nach der Morgenzeit des Empfängers, also nach der Übersicht.
|
||
- Erledigte oder abgebrochene Aufgabe vor dem Versand: Der Sender überspringt sie (`SKIPPED`, „Aufgabe schon erledigt“).
|
||
|
||
### 9a.3 Morgenübersicht
|
||
|
||
- Job-Schritt 5, je aktivem Nutzer mit `digestEnabled`. Schlüssel `digest:<YYYY-MM-DD>:<userId>`, also höchstens eine je Tag.
|
||
- Inhalt, jede Zeile nur mit dem nötigen Recht:
|
||
|
||
| Zeile | Beispiel | Recht |
|
||
|---|---|---|
|
||
| Aufgaben | „2 Aufgaben fällig, 1 davon überfällig.“ | `tasks:read` |
|
||
| Durchsicht | „Durchsicht fällig: 4 Völker am Stand Wald.“ | `colonies:read` und `inspections:read` |
|
||
| Vorschläge | „3 Vorschläge warten.“ (gezählt nach `requiredPermission`) | `tasks:read` |
|
||
| Saison | „Stand Wald: Schwarmzeit beginnt. Durchsicht alle 7 Tage.“ | `apiaries:read` |
|
||
|
||
- Erste Zeile „Guten Morgen. Heute für dich:“, Link `/dashboard`. `requiredPermissions` = alle Rechte, die eine Zeile genutzt hat.
|
||
- Ist keine Zeile zu füllen, entsteht keine Übersicht (weniger Lärm; Annahme).
|
||
- `digestTime` liegt zwischen 05:00 und 12:00. Der Job läuft um 04:30, die Übersicht ist also immer vorher fertig. Die Telegram-Zustellung bekommt `availableAt` = heute, `digestTime`, Europe/Berlin.
|
||
- Inhalt mit Stand von 04:30. Später angelegte Aufgaben kommen erst am nächsten Tag in die Übersicht; die Meldung je Aufgabe kommt trotzdem.
|
||
|
||
### 9a.4 Zustellung anlegen
|
||
|
||
In derselben Transaktion wie die `Notification` (`recordForUsers`/`recordForPermission`):
|
||
|
||
1. Telegram-Zustellung nur, wenn `telegramEnabled`, ein `TelegramLink` besteht und die Kategorie in `telegramCategories` steht. Standard: `TASK_DUE`, `DIGEST`; die übrigen schaltet der Nutzer zu.
|
||
2. `availableAt`: der spätere Wert aus „jetzt“, Kategorie-Zeit (9a.2, 9a.3) und Ende der Ruhezeit, falls „jetzt“ in der Ruhezeit liegt.
|
||
3. `pg_notify('notification_delivery', '')` nach dem Einfügen (wie Rückgrat).
|
||
|
||
Systemantworten des Bots (9a.5) umgehen die Ruhezeit, weil sie auf eine Handlung des Nutzers antworten.
|
||
|
||
### 9a.5 Verbinden und Trennen
|
||
|
||
1. **Profil > Benachrichtigungen > „Telegram verbinden“.** Erst ein Step-up wie beim Backup-Import: aktuelles Passwort, bei aktiver MFA dazu TOTP. Ein neuer Kanal nach außen ist eine sensible Änderung.
|
||
2. `POST /profile/telegram/link-token` erzeugt 32 Zufallsbytes, Base64url (43 Zeichen, erlaubt im `start`-Parameter). Gespeichert wird nur der SHA-256-Hash. Gültig 10 Minuten, nur einmal. Ein neues Token macht ältere offene Token des Nutzers ungültig. Antwort: `{ deepLink: 'https://t.me/<bot>?start=<token>', expiresAt }`. Drossel wie sensible Aktionen (10/min).
|
||
3. Die App zeigt einen großen Knopf „In Telegram öffnen“ und darunter „Gültig bis {Uhrzeit}“.
|
||
4. Telegram sendet `/start <token>`. Die Schleife `telegram-updates` verarbeitet jedes Update in einer eigenen Transaktion:
|
||
- Nur private Chats. Gruppen und Kanäle werden ignoriert.
|
||
- Token-Hash suchen; gültig heißt: nicht verbraucht, `expiresAt > now()` (DB-Uhr). Verbrauchen per Compare-and-Set.
|
||
- `TelegramLink` anlegen oder ersetzen. War der Chat mit einem anderen Nutzer verbunden, wird diese Verbindung gelöst; jener Nutzer bekommt eine Meldung in der App.
|
||
- `telegramEnabled = true`.
|
||
- Meldung in der App (`SYSTEM`): „Telegram verbunden mit „{Vorname}“ am {Datum}. Warst du das nicht? In den Einstellungen trennen.“ Das deckt einen mitgelesenen Link auf.
|
||
- Antwort `LINKED`: „Verbunden mit ApiCore. Hier kommen deine Erinnerungen. /stop beendet das.“
|
||
- Ungültiges, abgelaufenes oder benutztes Token: Antwort `LINK_INVALID`: „Der Link ist ungültig oder abgelaufen. Bitte in ApiCore neu verbinden.“ Kein Hinweis auf Konten.
|
||
- `updateOffset = update_id + 1` in derselben Transaktion. Jedes Update wirkt genau einmal.
|
||
5. **`/stop`:** Verbindung löschen, `telegramEnabled = false`, Antwort `STOPPED`: „Abgemeldet. Du bekommst keine Nachrichten mehr.“
|
||
6. **`/hilfe`, `/help` und jeder andere Text:** Antwort `HELP`: „Dieser Bot schickt Erinnerungen aus ApiCore. Verbinden geht nur in ApiCore unter Profil > Benachrichtigungen. /stop beendet.“ Höchstens 3 Antworten je Chat in 10 Minuten (Zähler im Empfänger; es gibt immer nur einen, siehe 9a.7).
|
||
7. **Trennen in der App:** `POST /profile/telegram/unlink` löscht die Verbindung und schickt `STOPPED` an den Chat.
|
||
|
||
### 9a.6 Versand (`telegram-send`)
|
||
|
||
- Neue Zeile in der Loop-Tabelle von `WorkerRuntime`: Takt 5 s, Weckkanal `notification_delivery`, Batch 25 per `claimBatch` (`FOR UPDATE SKIP LOCKED`), je Batch höchstens eine Zustellung je Nutzer bzw. Chat.
|
||
- **Vor jedem Senden neu prüfen**, sonst `SKIPPED` mit Grund:
|
||
- Nutzer aktiv, `telegramEnabled`, Verbindung besteht, Kategorie noch eingeschaltet;
|
||
- jedes Recht aus `requiredPermissions` noch vorhanden (`PermissionResolverService`);
|
||
- nicht veraltet: `TASK_DUE` und `DIGEST` nur am selben Tag (Europe/Berlin), alle anderen höchstens 24 h nach dem Anlegen.
|
||
- **Text:**
|
||
- Mit Details: `message` der Meldung, Leerzeile, `FRONTEND_ORIGIN + link` (Standard `/dashboard`).
|
||
- Ohne Details (`telegramDetail = false`): „Neue Meldung in ApiCore.“ plus Link.
|
||
- Klartext ohne `parse_mode`, also keine Formatierungs-Injektion über Aufgabentitel. Steuerzeichen entfernt, höchstens 600 Zeichen, Link-Vorschau aus.
|
||
- Nie Koordinaten, Adressen oder Notizen. Was in der Meldung steht, durfte der Nutzer schon beim Schreiben sehen und darf es beim Senden noch.
|
||
- **Grenzen der Bot API** (laut Telegram etwa 1 Nachricht je Sekunde und Chat, etwa 30 je Sekunde insgesamt): Der Sender hält höchstens 25 Nachrichten je Sekunde und je Chat mindestens 1,1 s Abstand.
|
||
- **Antworten:**
|
||
|
||
| Antwort | Wirkung |
|
||
|---|---|
|
||
| 200 | `DELIVERED`, `deliveredAt` |
|
||
| 429 mit `retry_after` | `availableAt = now() + retry_after`; zählt nicht als Fehlversuch |
|
||
| 403 (Bot blockiert) oder 400 „chat not found“ | Verbindung löschen, `telegramEnabled = false`, `SKIPPED`; Meldung in der App: „Telegram-Zustellung beendet. Der Bot wurde blockiert oder der Chat gelöscht.“ |
|
||
| 401 (Token ungültig) | Zustellung bleibt `PENDING` mit Wartezeit; Status unter „Verwaltung > System“; Meldung an `system:manage` einmal je Tag |
|
||
| 5xx, Timeout (10 s), Netzfehler | Wiederholung nach `retry-policy.ts`, nach 8 Versuchen `FAILED` |
|
||
|
||
### 9a.7 Empfang: Long Polling statt Webhook
|
||
|
||
Gewählt: **Long Polling im Worker** (`getUpdates`, `timeout=25`, `allowed_updates=["message"]`).
|
||
|
||
| | Long Polling (gewählt) | Webhook |
|
||
|---|---|---|
|
||
| Öffentlicher Eingang | keiner; nur ausgehend 443, das der Worker schon darf | neue öffentliche Route über Cloudflare und Traefik, Prüfung von `secret_token`, eigene Drossel |
|
||
| Angriffsfläche | keine neue | ein unauthentifizierter POST-Endpunkt im Internet |
|
||
| NetworkPolicy, Ingress | unverändert | Änderung an Ingress und Middleware |
|
||
| Kosten | eine gehaltene HTTPS-Verbindung, rund 3 500 kurze Anfragen am Tag | nur bei Nachrichten |
|
||
| Bedingung | genau ein Empfänger je Bot, kein Webhook gesetzt | – |
|
||
|
||
- **Genau ein Empfänger:** Lease in `TelegramPollState`, nur mit DB-Uhr: `UPDATE … SET leaseOwner = $me, leaseUntil = now() + interval '60 seconds' WHERE id = 'singleton' AND (leaseUntil IS NULL OR leaseUntil < now() OR leaseOwner = $me) RETURNING updateOffset`. Jede Runde verlängert. Wer die Lease nicht hat, wartet 30 s.
|
||
- 409 von Telegram („other getUpdates request“ oder „webhook is active“): `lastError` setzen, 30 s warten, Hinweis unter „Verwaltung > System“.
|
||
- Die Schleife hört auf das Stop-Signal (`AbortController` bricht den laufenden Abruf ab).
|
||
- Eine Störung des Empfangs macht den Worker nicht „ungesund“; sie steht nur unter „Verwaltung > System“.
|
||
|
||
### 9a.8 Einstellungen (Profil > Benachrichtigungen)
|
||
|
||
- Schalter „Erinnerung je Aufgabe“, „Morgenübersicht“ mit Uhrzeit (Standard 06:30), „Ruhezeit“ von–bis (Standard 21:00–06:30).
|
||
- Bereich Telegram: Status („Verbunden mit „{Vorname}“ seit {Datum}“ oder „Nicht verbunden“), Hauptknopf „Telegram verbinden“ bzw. „Trennen“, Auswahl der Kategorien, Schalter „Mit Details“.
|
||
- Ohne eingerichteten Bot: „Telegram ist nicht eingerichtet.“, kein Knopf.
|
||
- Alle Felder bearbeitbar, Null-Regel: `quietFrom: null` und `quietUntil: null` heißen „keine Ruhezeit“ (nur beide zusammen).
|
||
|
||
### 9a.9 Verwaltung > System
|
||
|
||
Neue Karte „Telegram“ auf der Seite aus Rückgrat Task 25, Daten aus der System-API (Rückgrat Task 24):
|
||
|
||
- Bot eingerichtet ja/nein, Bot-Name.
|
||
- Empfang: Inhaber der Lease, Alter des letzten Abrufs, letzter Fehler.
|
||
- Versand: offene Zustellungen, Alter der ältesten (Warnung ab 10 Minuten), `FAILED` der letzten 24 h, letzte Fehler (Code und Beschreibung, nie Token oder Chat-ID).
|
||
- „Erneut versuchen“ für `FAILED` (einzeln und alle), Recht `system:manage`.
|
||
|
||
### 9a.10 Konfiguration und Geheimnisse
|
||
|
||
| Variable | Ort | Pod | Zweck |
|
||
|---|---|---|---|
|
||
| `TELEGRAM_BOT_TOKEN` | eigenes Secret `apicore-telegram`, `optional: true` | nur Worker | Bot-Token; der Inhaber legt den Bot bei BotFather an und das Secret selbst |
|
||
| `TELEGRAM_BOT_USERNAME` | ConfigMap | api, Worker | Name für den Deep Link |
|
||
| `FRONTEND_ORIGIN` | vorhanden | Worker | Links in Nachrichten |
|
||
|
||
- Der API-Pod bekommt das Token nicht. Er braucht es für den Deep Link nicht.
|
||
- Startprüfung, wenn gesetzt: Format `^\d{5,}:[A-Za-z0-9_-]{30,}$`, in Produktion kein Platzhalterwert (wie Annahme zum Schlüsselbund). Sonst startet der Worker nicht.
|
||
- Der Bot-Client baut die URL intern. Fehler sind `TelegramApiError { method, statusCode, description }` ohne URL. Ein Test stellt sicher, dass das Token in keinem Log und keinem `lastError` landet.
|
||
- Bei BotFather: Gruppen nicht erlauben (`/setjoingroups` aus), kein Webhook setzen.
|
||
- Datenschutz: Nachrichten laufen über Server von Telegram und sind dort nicht Ende-zu-Ende verschlüsselt. Darum ist die Verbindung freiwillig, je Nutzer, mit Schalter „Mit Details“.
|
||
|
||
## 10. API
|
||
|
||
Alle Routen unter `/api/v1`, alle mit `@RequirePermission` (Abdeckung: `test/authorization-coverage.e2e-spec.ts`). Anlegen und Entscheiden mit `@Idempotent()`.
|
||
|
||
### 10.1 Neue Endpunkte
|
||
|
||
| Methode und Pfad | Recht | Zweck |
|
||
|---|---|---|
|
||
| `GET /suggestions?status=&colonyId=&apiaryId=` | `tasks:read` | Liste mit gerenderten Texten, gefiltert nach `requiredPermission`; Standard `OPEN` |
|
||
| `GET /suggestions/summary` | `tasks:read` | Anzahl offen je Priorität (Badge) |
|
||
| `POST /suggestions/:id/accept` | `tasks:create` | PLAN → Aufgabe(n). Body `{ dueDate?: string \| null; assignedUserId?: string \| null }`. 409 wenn nicht `OPEN` |
|
||
| `POST /suggestions/bulk-accept` | `tasks:create` | `{ ids: string[] }` (1–50), alles oder nichts |
|
||
| `POST /suggestions/:id/dismiss` | `tasks:update` | `{ note?: string }` (≤ 500) |
|
||
| `POST /suggestions/bulk-dismiss` | `tasks:update` | `{ ids: string[] }` (1–50) |
|
||
| `POST /suggestions/:id/reopen` | `tasks:update` | `DISMISSED` → `OPEN`, höchstens 30 Tage nach der Entscheidung |
|
||
| `POST /suggestions/:id/apply-cleanup` | `tasks:update`; zusätzlich `tasks:complete` für „Erledigt“, `queens:update` für die Königin | `{ tasks: { id; outcome: 'CANCEL' \| 'DONE' \| 'KEEP' }[]; queen?: { id; status: QueenStatus } \| null }`; nur IDs aus den Belegen |
|
||
| `GET /colonies/:id/next-steps` | `colonies:read` | `ColonyNextSteps` (8.5) |
|
||
| `GET /autopilot/today` | `colonies:read` | nächster Schritt über alle Völker, fällige Durchsichten je Stand, Anzahl Vorschläge, Abschnitt je Stand; Aufgaben nur mit `tasks:read` |
|
||
| `GET /phenology/state` | `apiaries:read` | je Stand Phase, Abschnitt, Quelle, GTS, Datenstand |
|
||
| `GET /phenology/reports?year=` | `phenology:read` | eigene Meldungen |
|
||
| `POST /phenology/reports` | `phenology:create` | `{ phase; date; apiaryId?: string \| null; notes? }`; Datum nicht in der Zukunft |
|
||
| `PATCH /phenology/reports/:id` | `phenology:update` | Null-Regel: `apiaryId: null` = alle Stände, `notes: null` leert; `phase`/`date` nicht leerbar |
|
||
| `POST /phenology/reports/:id/revoke` | `phenology:update` | zurückziehen statt löschen |
|
||
| `GET /varroa-measurements/thresholds?colonyId=&date=` | `varroa:read` oder `varroa:create` | Bänder je Methode und `postTreatmentUntil`; ersetzt `/varroa-measurements/threshold` (T3) |
|
||
| `GET /extractions/prefill?harvestIds=` | `extractions:create` | Σ Netto, Sorte, Quellen, Warnungen |
|
||
| `GET /honey-batches/prefill?extractionId=` | `honey-batches:create` | Restmenge, Sorte, Wassergehalt, Warnungen |
|
||
| `GET /honey-batches/:id/movements` | `honey-batches:read` | Bewegungsbuch |
|
||
| `POST /honey-batches/:id/movements` | `honey-batches:update` | `{ type: 'WITHDRAWAL' \| 'CORRECTION'; deltaKg; reason?; notes? }` |
|
||
| `GET /bottling/prefill?honeyBatchId=&productId=&date=` | `bottling:create` | Glasgröße, MHD, Wassergehalt, Warnungen |
|
||
| `POST /notifications/read-all` | angemeldet | eigene Meldungen gelesen |
|
||
| `GET /profile/notification-settings` | angemeldet (nur eigene; nie API-Schlüssel) | Einstellungen aus 9a.8 und Telegram-Status |
|
||
| `PATCH /profile/notification-settings` | angemeldet | Schalter, Zeiten (`HH:MM`, Morgenzeit 05:00–12:00), Kategorien, Details |
|
||
| `POST /profile/telegram/link-token` | angemeldet + Step-up (Passwort, ggf. TOTP), Drossel 10/min | Deep Link, 10 min gültig (9a.5) |
|
||
| `POST /profile/telegram/unlink` | angemeldet | Verbindung lösen |
|
||
| `GET /system/notification-deliveries?status=` | `system:manage` | Warteschlange, Fehler, Empfangsstatus (9a.9) |
|
||
| `POST /system/notification-deliveries/:id/retry`, `POST /system/notification-deliveries/retry-failed` | `system:manage` | `FAILED` → `PENDING` |
|
||
|
||
### 10.2 Geänderte Endpunkte
|
||
|
||
| Endpunkt | Änderung |
|
||
|---|---|
|
||
| `POST /inspections` | `applyToColony`; Antwort + `colonyTakeover` (7.9); `feedStatus` ist `ColonyCondition` |
|
||
| `POST/PATCH /harvests`, `/extractions`, `/honey-batches`, `/bottling` | Antwort + `warnings` |
|
||
| `POST /bottling` | `acknowledgedWarnings`; `jarSize`/`bestBeforeDate` optional mit Artikel; 409 `CONFIRMATION_REQUIRED` |
|
||
| `PATCH /honey-batches/:id` | `amount` bucht `CORRECTION`; `null` bei Buchungen → 400 |
|
||
| `POST /colonies/:id/split` | `date?`, `queenSource?` |
|
||
| `POST /varroa-measurements` (und `/bulk`, T3) | `sampleBees`; Prozent bei Puderzucker/Auswaschung |
|
||
| `GET /colonies/:id/varroa-measurements` | je Messung `rating: { light; reasonCode; band }` |
|
||
| `GET /apiaries/:id/colony-overview` (T3) | + `inspectionDueOn`, `segment`, `nextStepTitle`; `openWarnings`/`openCritical` aus Vorschlägen |
|
||
| `GET /apiaries/:id/offline-pack` (T3) | + `nextSteps` je Volk (optional; Version bleibt 1) |
|
||
| `GET /calendar` | Typ `WITHDRAWAL` mit `endDate` |
|
||
| `GET /notifications`, `POST /notifications/:id/read` | nur eigene; Antwort + `category`, `link` |
|
||
| `POST/PATCH /colonies`, `GET /colonies/:id` | + `occupiedFrames` (0–40); `strength` bleibt als veralteter Altwert (4.14) |
|
||
| `POST/PATCH /inspections` | `colonyStrength` höchstens 40 |
|
||
| `PATCH /automation/rules/:id` | Config je Regel validiert |
|
||
| `GET /dashboard/alerts` | `OLD_QUEEN` mit `queenMaxAgeYears` |
|
||
|
||
### 10.3 Warnungen (ein Format)
|
||
|
||
```ts
|
||
interface PlausibilityWarning {
|
||
code: 'WITHDRAWAL_ACTIVE' | 'EXTRACTION_EXCEEDS_HARVEST' | 'HARVEST_NET_MISSING' | 'BATCHES_EXCEED_EXTRACTION'
|
||
| 'WATER_CONTENT_HIGH' | 'WATER_CONTENT_ABOVE_LEGAL' | 'WATER_CONTENT_MISSING' | 'JAR_SIZE_DIFFERS';
|
||
severity: 'HINWEIS' | 'WARNUNG' | 'BESTAETIGEN';
|
||
message: string; // deutsch, serverseitig gerendert
|
||
refs: { type: string; id: string }[]; // nur IDs
|
||
}
|
||
```
|
||
|
||
Ein Ort: `src/common/warnings/`. Prefill-Endpunkte liefern dieselben Warnungen schon vor dem Speichern.
|
||
|
||
## 11. Oberfläche
|
||
|
||
### 11.1 Grundsätze
|
||
|
||
- Ein Hauptknopf je Ansicht. Alles Weitere ist sekundär oder im Menü.
|
||
- Begründungen in höchstens zwei kurzen Sätzen. Zustand nie nur über Farbe: Icon plus Wort.
|
||
- Vorbelegt wird nur, was keine Beobachtung ist (Glasgröße, MHD aus dem Artikel). Messwerte kommen als Hinweis mit „übernehmen“.
|
||
- Vor Next.js-Code `node_modules/next/dist/docs/` lesen (`ApiCore-Frontend/AGENTS.md`).
|
||
|
||
### 11.2 Seiten
|
||
|
||
| Seite | Änderung | Hauptaktion |
|
||
|---|---|---|
|
||
| Start/Heute (`/dashboard`) | „Nächster Schritt“ aus `GET /autopilot/today` (offline wie bisher aus Aufgaben). Karte „Durchsicht fällig“ je Stand mit Völkerliste. Chips „Vorschläge {n}“ und Saison je Stand („Schwarmzeit · alle 7 Tage“). | der eine nächste Schritt |
|
||
| Vorschläge (`/suggestions`, neu) | Liste nach Stand gruppiert (11.3) | je Karte „Übernehmen“ oder „Erfassen“ |
|
||
| Volk (`/colonies/[id]`) | Karte „Als Nächstes“ oben (3 Schritte). Varroa-Liste mit Jahreszeit-Ampel. Aktion „Ableger bilden“ mit Datum und Herkunft der Königin. | erster Schritt |
|
||
| Stand (`/apiaries/[id]`, T3) | Kopf mit Saison und Quelle, Knopf „Pflanze blüht“ (Sheet). Zeilen zeigen „Durchsicht fällig in 2 Tagen“ bzw. „seit 3 Tagen“ und den nächsten Schritt. | „Runde starten“ (T3) |
|
||
| Durchsicht neu (T3) | Abschnitt „Volkskarte“ über „Speichern“ (7.9) | „Speichern“ |
|
||
| Varroa neu (T3) | Ampel aus Bändern; Feld „Probe (Bienen)“ bei Puderzucker/Auswaschung mit Chip „500 Bienen“; Hinweis „Behandlungseffekt“ | „Speichern“ |
|
||
| Schleuderung, Honigcharge | Hinweise „Aus 2 Ernten: 42,5 kg · übernehmen“; Warnliste über „Speichern“ | „Speichern“ |
|
||
| Honigcharge Detail | Karte „Bewegungen“ (Muster wie `components/stock/stock-card`); „Menge korrigieren“ bucht | – |
|
||
| Abfüllung neu | Glasgröße und MHD aus dem Artikel mit Herkunft; Bestätigungsdialog bei über 20 % | „Abfüllen“ |
|
||
| Kalender | Wartezeiten als Spanne | – |
|
||
| Verwaltung > Automatisierung | Formulare für die neuen Regeln und Werte; Quelle je Wert sichtbar | „Speichern“ |
|
||
| Meldungen (Glocke) | „Alle gelesen“; Meldungen mit `link` öffnen das Ziel | – |
|
||
| Volk bearbeiten | Feld „Besetzte Waben“ (`NumberStepper`, 0–40); „Volksstärke (alt, Skala 1–10)“ nur, wenn gespeichert | „Speichern“ |
|
||
| Profil > Benachrichtigungen (neu) | Schalter, Uhrzeiten, Telegram-Bereich (9a.8) | „Telegram verbinden“ bzw. „Speichern“ |
|
||
| Verwaltung > System | Karte „Telegram“ (9a.9) | – |
|
||
|
||
Die Karte „Durchsicht fällig“ startet mit „Runde starten“ eine T3-Runde nur mit den fälligen Völkern dieses Stands (`startRound` mit gefilterter Liste, Reihenfolge nach Code).
|
||
|
||
### 11.3 Vorschlagsliste
|
||
|
||
- **Kopf:** „{n} Vorschläge“ und ein Satz: „Übernehmen plant eine Aufgabe. Verwerfen blendet den Vorschlag aus.“
|
||
- **Gruppen:** je Stand, dann „Betrieb“ (Lager, Rohstoffe). In der Gruppe nach Rang (8.4).
|
||
- **Zusammengefasst:** Gleiche Absicht im selben Stand wird eine Zeile:
|
||
- `INSPECT`: „Durchsicht fällig · 6 Völker“ mit „Runde starten“.
|
||
- PLAN-Absichten: „Honigraum prüfen · 3 Völker“ mit „Alle übernehmen“ (`bulk-accept`). Aufklappen zeigt die einzelnen Völker.
|
||
- **Karte** (`SuggestionCard`): Icon der Absicht, Titel, Begründung, Fälligkeit (`DueChip`), Hauptknopf („Übernehmen“, „Erfassen“ oder „Aufräumen“), daneben „Verwerfen“ als Text-Knopf. Menü: „Als Aufgabe planen“ (DO), „Termin ändern“ (PLAN; kleines Datums-Sheet, dann Übernehmen).
|
||
- **Nach dem Übernehmen:** Toast „Aufgabe angelegt · Ansehen“.
|
||
- **Nach dem Verwerfen:** Toast „Verworfen · Rückgängig“ (8 s). Unten der Link „Verworfene (30 Tage)“ mit „Wieder öffnen“. Damit hängt nichts an der Toast-Zeit (WCAG 2.2.1).
|
||
- **Leer:** „Keine offenen Vorschläge.“
|
||
- **Navigation:** „Vorschläge“ in `nav-config.ts`, Gruppe „Arbeit“, mit Zähler, Recht `tasks:read`.
|
||
|
||
### 11.4 Bausteine
|
||
|
||
- **Wiederverwendet:** `Sheet`, `ChoiceGroup`, `ToggleChip`, `ConfirmDialog`, `DueChip`, `TaskSourceBadge`, `StatusLightBadge`, `EmptyState`, `QueryState`, `PageHeader`, `FormSection`, `StickySubmit`, `NumberStepper`, `PreviousValueHint` (Präfix-Prop), `RecordSelect`, `EnumSelect`, `useCaptureMutation`, `emptyToNull`, `toApiDate`, Runden-Funktionen aus T3.
|
||
- **Neu:** `SuggestionCard`, `SuggestionGroup`, `NextStepsCard`, `SeasonChip`, `PhenologyReportSheet`, `WarningList`, `ColonyTakeoverSection`, `HoneyBatchMovementsCard`, `NotificationSettingsCard`, `TelegramLinkCard` (Step-up über den vorhandenen Passwort-/TOTP-Dialog des Backup-Imports).
|
||
- **Neue Lib-Module:** `lib/suggestions`, `lib/autopilot`, `lib/phenology`, `lib/warnings`, `lib/notification-settings`, `lib/colonies/condition.ts` (`FEED_CONDITION_LABELS`, `ratingToCondition`).
|
||
- `PhenologyReportSheet`: Zeilen mit 56 px (`min-h-14`), je Phase Name und Pflanze. Vorgewählt ist „für alle Stände“. Ein Tipp meldet für heute. Offline über `useCaptureMutation` in die Warteschlange.
|
||
- Varroa-Diagramm (T3): Bänder als Stufenlinie je Monat. Beim Bau den `dataviz`-Skill laden.
|
||
|
||
### 11.5 Barrierefreiheit
|
||
|
||
- Touch-Ziele mindestens 44 px, Sheet-Zeilen 56 px.
|
||
- Warnungen und Live-Werte mit `role="status"` und `aria-live="polite"`; Bestätigung als Dialog mit Fokusfalle und Rückkehr des Fokus.
|
||
- Ampeln immer mit Icon und Wort („Gut“, „Achtung“, „Kritisch“, „Behandlungseffekt“).
|
||
- Zusammengefasste Gruppen sind Buttons mit `aria-expanded`.
|
||
- Kontrast über die `condition-*`-Tokens.
|
||
|
||
## 12. Sicherheit
|
||
|
||
- **Rechte:** Jede neue Route mit `@RequirePermission`. Vorschläge und Schritte werden je Eintrag nach `requiredPermission` gefiltert. Wer kein `varroa:read` hat, sieht weder „12,4 Milben pro Tag“ noch den Vorschlag. Gleiches gilt für Behandlungen (`treatments:read`) und Durchsichten (`inspections:read`).
|
||
- **Keine Rechteausweitung:** „Übernehmen“ verlangt `tasks:create`. Aufräumen prüft jedes Teilrecht einzeln. Die Volkskarte ändert sich nur mit `colonies:update`.
|
||
- **Benachrichtigungen** nur an Nutzer mit passendem Recht (Fan-out über `recordForPermission`). Kein Leck über Rundmeldungen mehr. Bewusste Abweichung von Rückgrat-Spec §4, siehe Entscheidungen.
|
||
- **Telegram:** Rechte beim Schreiben und beim Senden geprüft; Token nur im Worker, eigenes Secret, nie in Logs; Verbinden nur mit Step-up, Einmal-Token als Hash, 10 Minuten; Meldung in der App bei jeder neuen Verbindung; kein öffentlicher Eingang (Long Polling); Klartext ohne `parse_mode`; Schalter „Mit Details“. Einzelheiten 9a.5–9a.10.
|
||
- **Netz:** Nur Worker-Schleifen rufen Wetterdienste und Telegram auf. Feste Hosts aus der Konfiguration, numerische Koordinaten, also kein SSRF-Weg. Handler und Jobs bleiben ohne Netz (Wächter). NetworkPolicies unverändert.
|
||
- **Standort:** Koordinaten gehen auf 0,01° gerundet hinaus (Diebstahlschutz, wie Rückgrat-Spec §5.4). `GET /phenology/state` gibt keine Koordinaten aus.
|
||
- **Eingaben:** Config je Regel validiert; Ober- und Untergrenzen; Listen begrenzt (Bulk 50, `applyToColony` 3). Freitexte (`note`) mit Längengrenze.
|
||
- **Payloads und Logs:** Ereignisse und Belege tragen nur IDs, Enums, Zahlen, Daten. Fehler über `loggableError()`.
|
||
- **Integrität:** Entscheidungen per Compare-and-Set; Ledger per Zeilensperre; Audit-Einträge in derselben Transaktion.
|
||
- **Idempotenz:** alle neuen POSTs der Warteschlange mit `@Idempotent()`.
|
||
|
||
## 13. Fehlerfälle
|
||
|
||
| Fall | Verhalten |
|
||
|---|---|
|
||
| Worker fällt aus | Vorschläge und Schritte kommen verspätet. Die Übernahme in die Volkskarte läuft synchron und ist nicht betroffen. Der Job gleicht täglich alles nach. |
|
||
| Wetterdienst nicht erreichbar | Nächster Takt versucht es. Die Phase nutzt die letzte GTS mit „Stand: {Datum}“. Ist sie älter als 3 Tage, gilt der Kalender. |
|
||
| Stand ohne Koordinaten | Nur Meldung und Kalender; Hinweis in der Begründung. |
|
||
| Lückenhafte Wetterdaten (> 10 %) | GTS nicht benutzt, Hinweis „Wetterdaten lückenhaft“. |
|
||
| Zwei Handler für dasselbe Volk gleichzeitig | Zeilensperre auf dem Volk; der zweite sieht den Stand des ersten. |
|
||
| Doppeltes „Übernehmen“ (Doppeltipp, zwei Geräte) | Gleicher Idempotency-Key → gespeicherte Antwort. Anderer Key → 409 „Der Vorschlag ist nicht mehr offen.“ mit aktuellem Status. Genau eine Aufgabe. |
|
||
| Übernehmen nach Ende des Volks | Vorschlag ist `OBSOLETE` → 409. |
|
||
| Verworfen, Ursache besteht weiter | Kein neuer Vorschlag für denselben Anker. Neuer Anker → neuer Vorschlag. |
|
||
| Ungültige Regel-Config | `PATCH` → 400 mit Feld. Ein von Hand verdorbener Wert in der DB → Standardwert, Log-Warnung. |
|
||
| Offline-Durchsicht mit Volkskarte kommt spät | Neuere Durchsicht vorhanden → Feld übersprungen (`NEWER_INSPECTION`), Durchsicht gespeichert. |
|
||
| Volkskarte ohne `colonies:update` | Durchsicht 201, Felder übersprungen (`NO_PERMISSION`). |
|
||
| Futter-Freitext in alten Daten oder Backups | Abbildung nach 4.4, Rest in `notes`. Nichts geht verloren. |
|
||
| Zwei Abfüllungen gleichzeitig über den Rest | Ledger-Sperre: eine gelingt, die andere 400 mit Resttext. |
|
||
| Mengenkorrektur unter 0 | 400 „Die Menge kann nicht unter 0 sinken.“ |
|
||
| Wassergehalt über Grenze ohne Bestätigung | 409 `CONFIRMATION_REQUIRED`; Formular zeigt Dialog; bestätigte Wiederholung 201. Aus der Warteschlange: Eintrag bleibt mit Servertext, Tipp öffnet den Dialog. |
|
||
| Wartezeit-Konflikt auf drei Stufen | Warnung in jeder Antwort, Meldung nur einmal je (Ernte, Volk). |
|
||
| Meldung einer Phase in der Zukunft | 400 „Das Datum liegt in der Zukunft.“ |
|
||
| Falsch getippte Meldung | „Zurückziehen“; Zustand wird neu berechnet; Phase kann zurückgehen. |
|
||
| Phase rückwärts (zurückgezogene Meldung) | `SeasonPhaseChanged` mit Quelle; offene DO-Vorschläge passen sich beim Abgleich an. |
|
||
| Job scheitert in Schritt 3 | Schritte 1, 2, 4 und 5 laufen trotzdem. `JobRun` `FAILED` mit Zählern; Meldung an `automation:update` (einmal je Tag). Handler halten die betroffenen Völker aktuell. |
|
||
| Autopilot ausgeschaltet | Offene Autopilot-Vorschläge → `RESOLVED`; Regel- und Kettenvorschläge bleiben. |
|
||
| Alter Backup-Import | Abbildungen nach 4.13; Ledger-Invariante danach geprüft. Telegram-Verbindungen sind nicht im Backup; Nutzer verbinden neu. |
|
||
| Volk mit altem `strength`, ohne `occupiedFrames` | Anzeige „Volksstärke (alt): 7 von 10“; nichts wird umgerechnet. Die nächste Durchsicht mit Übernahme setzt `occupiedFrames`. |
|
||
| `colonyStrength` über 40 | 400 „Höchstens 40 besetzte Waben.“; die Prüfabfrage aus Migration 10 findet Altfälle vor dem Deploy. |
|
||
| Job läuft nicht (CronJob gescheitert) | Keine Morgenübersicht und keine Meldung je Aufgabe an diesem Tag. Die Meldung „Job fehlgeschlagen“ kommt in der App und, falls eingeschaltet, per Telegram (Kategorie `SYSTEM`). Am Folgetag holt das Nachholfenster `TaskDue` nach; veraltete Übersichten werden nicht nachgeschickt. |
|
||
| Telegram nicht erreichbar | Zustellungen bleiben `PENDING`, Wiederholung nach Rückgrat-Regel, nach 8 Versuchen `FAILED`. In der App ist alles da. |
|
||
| Telegram meldet 429 | Warten laut `retry_after`, kein Fehlversuch. |
|
||
| Nutzer blockiert den Bot | 403 → Verbindung gelöst, Zustellung `SKIPPED`, Hinweis in der App. |
|
||
| Bot-Token falsch oder widerrufen | 401 → Zustellungen warten; „Verwaltung > System“ zeigt „Bot-Token ungültig“; Meldung an `system:manage` einmal je Tag. |
|
||
| Zwei Worker | Versand über `SKIP LOCKED` ohne Dubletten; Empfang nur beim Inhaber der Lease. |
|
||
| Ein zweiter Prozess pollt denselben Bot (z. B. Testumgebung mit Produktions-Token) | 409 von Telegram, `lastError` sichtbar; Abhilfe: je Umgebung ein eigener Bot. |
|
||
| Deep Link mitgelesen, Fremder verbindet zuerst | Token einmalig und 10 Minuten gültig; der Nutzer sieht sofort „Telegram verbunden mit „{Vorname}““ in der App und trennt mit einem Tipp. |
|
||
| Rechte entzogen zwischen Schreiben und Senden | Prüfung beim Senden → `SKIPPED`. |
|
||
| Zustellung veraltet (Worker lange aus) | Übersicht und Aufgaben-Meldung nur am selben Tag, Rest höchstens 24 h; danach `SKIPPED`. |
|
||
| Morgenzeit vor 05:00 gewünscht | 400 „Bitte eine Uhrzeit zwischen 05:00 und 12:00 wählen.“ |
|
||
|
||
## 14. Tests
|
||
|
||
TDD: zuerst der fehlschlagende Test. Testnamen englisch wie im Bestand.
|
||
|
||
### 14.1 Unit (Backend)
|
||
|
||
- `varroa-bands.spec.ts` als Tabelle: `10.0/Tag im August → yellow`, `10.1 → red`, `0.6 im November → red`, `Jungvolk 5.1 im Juni → red`, `Puderzucker 5.1 % im Juli → red`, `Puderzucker im April → no band`, `measurement 5 days after treatment end → no light, post-treatment`.
|
||
- `inspection-due.spec.ts`: Intervall je Abschnitt, `WINTERRUHE → null`, ohne Durchsicht ab `createdAt`, erste Durchsicht im Jahr.
|
||
- `gts.spec.ts`: Faktoren 0,5/0,75/1, negative Tage zählen 0, Lückenquote, 200 °C.
|
||
- `phenology-phase.spec.ts`: Maximum aus Meldung/GTS/Kalender, zurückgezogene Meldungen zählen nicht, „alle Stände“ gilt je Stand, Kalibrierung als Median der Vorjahre, ohne Schwelle Kalender.
|
||
- `season-segment.spec.ts`: Phase → Abschnitt inkl. eingestelltem Schwarmzeit-Beginn.
|
||
- `autopilot-rules.spec.ts`: je Regel A1–A10 eine Zeile „gegeben → erwartet“, inklusive Anker und Begründungscode.
|
||
- `next-step-ranking.spec.ts`: Tupel-Reihenfolge, `IN_PROGRESS` zuerst, Zusammenlegen gleicher Formulare, höchstens 3, stabile Reihenfolge über `id`.
|
||
- `suggestions.service.spec.ts`: `proposeIn` respektiert offene Aufgabe und verworfenen Anker; `accept` legt Aufgaben über `createInTx` an; CAS bei zweitem Übernehmen.
|
||
- `suggestion-texts.spec.ts`: jeder `reasonCode` rendert, höchstens 2 Sätze, deutsche Zahlen.
|
||
- `rule-config.spec.ts`: je Trigger Standard bei ungültigen Werten; Ablehnung beim Update.
|
||
- `colony-takeover.spec.ts`: Abbildung und alle Überspring-Gründe.
|
||
- `feed-vocabulary.spec.ts`: Tabelle aus 4.4.
|
||
- `honey-batch-ledger.spec.ts`: Buchen, zu wenig Rest, Grammrundung, Korrektur als Differenz, Null-Regeln.
|
||
- `water-content.spec.ts`: jüngste Messung gewinnt, Sorte, Bestätigungsstufe.
|
||
- `withdrawal-window.spec.ts`: ein Helfer; `getWithdrawalStatus` und `withdrawalEndDay` liefern dasselbe Fenster.
|
||
- `notifications.service.spec.ts`: `record` ohne `userId` kompiliert nicht; `recordForPermission` vergibt den Schlüssel je Empfänger; legt eine Telegram-Zustellung nur bei eingeschalteter Kategorie und bestehender Verbindung an; `availableAt` nach Ruhezeit und Kategorie-Zeit.
|
||
- `colony-lifecycle.spec.ts`: `releaseQueenIn` beendet Historie und `currentQueenId`; `update` auf `SOLD` und `DECEASED` ruft ihn auf.
|
||
- `occupied-frames.spec.ts`: 0–40, `strength` bleibt beim Schreiben von `occupiedFrames` unberührt.
|
||
- `task-due.handler.spec.ts`: Zuständiger bevorzugt; sonst `tasks:complete`; nichts bei `taskDueEnabled = false`; Schlüssel je Aufgabe, Tag und Nutzer.
|
||
- `digest.spec.ts`: Zeilen nur mit Recht; keine Übersicht ohne Inhalt; `requiredPermissions` vollständig; eine je Tag.
|
||
- `telegram-link.spec.ts`: Token 43 Zeichen Base64url, nur Hash gespeichert, abgelaufen/benutzt → `LINK_INVALID`, neues Token entwertet alte, Chat wechselt den Nutzer mit Hinweis.
|
||
- `telegram-updates.spec.ts`: `/start`, `/stop`, `/hilfe`, Gruppen ignoriert, Antwortgrenze je Chat, Offset in derselben Transaktion, Lease nur mit DB-Uhr.
|
||
- `telegram-send.spec.ts`: Rechte und Verbindung beim Senden geprüft; Klartext ohne `parse_mode`, höchstens 600 Zeichen; 429 → `retry_after` ohne Fehlversuch; 403 → Verbindung gelöst; 401 → wartet; veraltet → `SKIPPED`.
|
||
- `telegram-api-error.spec.ts`: Token erscheint weder in `TelegramApiError` noch in `loggableError()`.
|
||
- `weather-source.spec.ts`: Auswahl über `WEATHER_HISTORY_SOURCE`; nur `https:`-URLs; API-Schlüssel nie in Fehlertexten.
|
||
|
||
### 14.2 e2e (Test-DB, mit `drainDomainEvents`)
|
||
|
||
- `suggestions.e2e-spec.ts`:
|
||
- `an inspection without queen and eggs leaves one OPEN suggestion and no task`
|
||
- `accept creates exactly one task with sourceSuggestionId and a calendar delivery`
|
||
- `a second accept with another key answers 409`
|
||
- `a dismissed suggestion is not proposed again for the same inspection`
|
||
- `a later inspection with the same finding proposes again`
|
||
- `varroa-seasonal.e2e-spec.ts`: `12 mites/day in August proposes a treatment check`; `12 mites/day five days after treatment end proposes nothing`; `thresholds for a young colony are halved`.
|
||
- `chains.e2e-spec.ts`: Weiselzellen → +7; `RAISE_OWN` → Übernehmen ergibt 3 Aufgaben mit +9/+21/+28; Zuweisung → +7; Ableger `QUEEN` plus Zuweisung → ein Vorschlag.
|
||
- `colony-ended.e2e-spec.ts`: Auswinterungsverlust mit `markColonyDeceased` beendet die Königin-Zuordnung in derselben Transaktion; offene Vorschläge `OBSOLETE`; Aufräumen bricht 2 Aufgaben ab und erledigt 1; 403 ohne `tasks:complete`.
|
||
- `autopilot.e2e-spec.ts`: Schwarmzeit per Meldung, letzte Durchsicht vor 9 Tagen → Schritt 1 `INSPECT` überfällig; nach neuer Durchsicht `RESOLVED`; nie mehr als 3 Schritte; ohne `varroa:read` keine Varroa-Gründe.
|
||
- `phenology.e2e-spec.ts`: Meldung „alle Stände“ → jeder Stand `SALWEIDE`, `SeasonPhaseChanged` einmal je Stand; Zurückziehen rechnet neu; Wiederholung mit gleichem Key ergibt eine Meldung; Zukunftsdatum 400; 403 ohne Recht.
|
||
- `daily-evaluation.e2e-spec.ts` (erweitert): zweimal laufen ergibt gleiche Vorschlagszahlen; Ablauf nach `expiresAt`; `InspectionOverdue` nach Abschnitts-Intervall.
|
||
- `weather-history.e2e-spec.ts` (Fetch gemockt): fehlende Tage einmal geholt, gerundete Koordinaten, ungültige Werte `null`, Fehler ohne Zeilen.
|
||
- `withdrawal-chain.e2e-spec.ts`: Ernte im Fenster → 201 + Warnung + eine Meldung; Schleuderung und Abfüllung → Warnung, keine zweite Meldung; `WithdrawalEnded` → eine INFO; Kalender-Spanne in der Outbox.
|
||
- `honey-chain.e2e-spec.ts`: Vorbelegung summiert Netto; beide Plausibilitätswarnungen; Ledger-Invariante nach Anlegen, Abfüllen, Korrigieren, Entfernen; zwei parallele Abfüllungen über den Rest → genau eine 400.
|
||
- `honey-ledger-migration.e2e-spec.ts`: Altdaten mit Abfüllungen → Invariante hält, Buchungen tragen `sourceBottlingBatchId`.
|
||
- `bottling-quality.e2e-spec.ts`: 19 % → 201 mit Warnung; 20,5 % → 409; bestätigt → 201; MHD aus `shelfLifeMonths`; abweichendes Glas → Hinweis.
|
||
- `inspection-takeover.e2e-spec.ts`: Futter und Brut übernommen, Audit `Colony UPDATE`; neuere Durchsicht → übersprungen; ohne `colonies:update` → 201 und übersprungen.
|
||
- `notifications.e2e-spec.ts`: Lesen je Nutzer getrennt; Nutzer ohne `colonies:read` bekommt keine Volksmeldung; Migration verteilt Rundmeldungen.
|
||
- `colony-strength.e2e-spec.ts`: Altwert `strength` bleibt nach Migration und PATCH von `occupiedFrames` erhalten; Übernahme aus der Durchsicht schreibt nur `occupiedFrames`; 41 → 400.
|
||
- `reminders.e2e-spec.ts`: `TaskDue` nach Drain → genau eine Meldung je Empfänger, auch nach erneuter Zustellung; Job zweimal → eine Übersicht je Nutzer; Übersicht ohne Varroa-/Durchsichtszeile für einen Nutzer ohne die Rechte.
|
||
- `telegram.e2e-spec.ts` (Bot API gemockt, kein echter Netzverkehr):
|
||
- `link token -> /start links the chat once; a second /start with the same token answers LINK_INVALID`
|
||
- `link-token requires step-up and is throttled`
|
||
- `a due task yields one delivery after the morning time and none inside quiet hours`
|
||
- `two send loops in parallel deliver each message exactly once`
|
||
- `only the lease holder polls; a second worker waits`
|
||
- `a revoked permission skips the delivery at send time`
|
||
- `GET /system/notification-deliveries shows FAILED entries and retry moves them back to PENDING`
|
||
- `authorization-coverage.e2e-spec.ts` deckt alle neuen Routen ab (Profil-Routen nie für API-Schlüssel).
|
||
|
||
### 14.3 Wächter
|
||
|
||
- Handler-IDs unter `WorkerModule` = unter `AppModule` (inkl. aller T4-Handler).
|
||
- `honey-batch-amount-writes.spec.ts`: `amount` der Honigcharge wird nur im Ledger geschrieben.
|
||
- `no-network-outside-loops.spec.ts`: Wetter-Hosts, `api.telegram.org` und `fetch(` nur in `src/weather/` bzw. `src/telegram/`; nie in `*.handlers.ts` oder `src/jobs/`. `handler-contract.spec.ts` verbietet zusätzlich Importe von `telegram-client`.
|
||
- `colony-end-releases-queen.spec.ts`: jede Stelle, die einen beendeten Status setzt, ruft `releaseQueenIn` auf.
|
||
- `queen-age-threshold.spec.ts`: keine zweite Altersschwelle im Code (`OLD_QUEEN_AGE_YEARS` existiert nicht mehr; Dashboard und Job lesen `queenMaxAgeYears`).
|
||
- `no-broadcast-notifications.spec.ts`: kein `notification.create`/`createMany` ohne `userId`.
|
||
- Startprüfung: `TELEGRAM_BOT_TOKEN` mit falschem Format oder Platzhalter → Worker startet in Produktion nicht.
|
||
- Jeder `AutomationTriggerType` hat `DEFAULT_RULES`, Config-Parser und, falls vorschlagend, Textvorlagen.
|
||
- Jeder `SuggestionIntent` hat Textvorlage (Backend) sowie Icon und Label (Frontend, Vitest).
|
||
- Backup-Wächter für alle neuen Modelle (4.13); `update-dto-null-handling.spec.ts` für alle neuen und geänderten DTOs.
|
||
- `after-commit-usage.spec.ts` mit der verkürzten Altpfad-Liste.
|
||
|
||
### 14.4 Frontend (Vitest/RTL)
|
||
|
||
- Vorschlagsliste: Gruppierung, „Alle übernehmen“, Verwerfen mit Rückgängig, „Verworfene“, Leerzustand, Namen der Knöpfe.
|
||
- `NextStepsCard`: höchstens 3, Begründung, Aktionen führen mit `colonyId` ins richtige Formular.
|
||
- Heute: Quelle des nächsten Schritts, Karte „Durchsicht fällig“ startet eine Runde nur mit fälligen Völkern.
|
||
- `PhenologyReportSheet`: ein Tipp meldet, 56-px-Zeilen, „für alle Stände“ vorgewählt, offline in der Warteschlange.
|
||
- `ColonyTakeoverSection`: nur ausgefüllte Felder angehakt, „bisher → neu“, ohne Recht unsichtbar.
|
||
- Varroa-Ampel mit Bändern und Hinweis „Behandlungseffekt“; Abfüll-Bestätigung; MHD aus dem Artikel; Bewegungsbuch-Karte; Kalender-Spanne.
|
||
- Volk bearbeiten: „Besetzte Waben“ 0–40; Altfeld nur mit Wert; Leeren sendet `null`.
|
||
- `NotificationSettingsCard`: Uhrzeiten validiert, Ruhezeit nur paarweise leerbar, Speichern sendet nur Geändertes.
|
||
- `TelegramLinkCard`: Step-up-Dialog vor dem Link, Knopf „In Telegram öffnen“ mit Ablaufzeit, Status „Verbunden mit …“, „Trennen“, Hinweis ohne eingerichteten Bot.
|
||
- Glocke: Meldung mit `link` öffnet das Ziel.
|
||
|
||
## 15. Rollout
|
||
|
||
1. **Reihenfolge:** Rückgrat-Tasks 3–18, 24 und 25 und die T3-Tasks 2, 3, 9, 11, 14 sind gemergt. Dann T4 in drei Lieferungen (Abschnitt 16).
|
||
2. **Migrationen** in der Reihenfolge aus 4.12. Vor dem Deploy ein Backup ziehen (App-Backup und `pg_dump`). Nach dem Deploy die Prüfabfragen aus 4.12 ausführen.
|
||
3. **Regeln:** `ensureDefaults` legt die neuen Regeln an. Bestehende Configs bleiben; `VARROA_THRESHOLD` liest den alten Wert weiter. `TREATMENT_FOLLOW_UP`/`FEEDING_FOLLOW_UP` bleiben, wie sie sind.
|
||
4. **Bestehende Automatik-Aufgaben** bleiben Aufgaben (9.5).
|
||
5. **Benachrichtigungen:** Nach Migration 8 sieht jeder Nutzer seine Kopien. Ungelesene Rundmeldungen bleiben ungelesen.
|
||
6. **Worker:** neue Schleifen `phenology-weather`, `telegram-send` und `telegram-updates`. Der erste Wetter-Takt holt die Tage seit dem 1. Januar nach (je gerundetem Punkt eine Archiv- und eine Forecast-Anfrage). NetworkPolicy und CronJobs unverändert; `daily-evaluation` braucht etwas länger (Limit 1800 s reicht).
|
||
7. **Volksstärke:** Migration 10 ändert keine Daten. Vor dem Deploy die Prüfabfrage auf `colonyStrength > 40` laufen lassen; Treffer mit dem Inhaber klären, nicht kürzen.
|
||
8. **Wetterquelle:** `WEATHER_HISTORY_SOURCE=open-meteo` in der ConfigMap (Inhaber-Entscheidung, freie API). Ein späterer Wechsel ist eine Änderung der ConfigMap (bezahltes Open-Meteo) bzw. ein neuer Adapter plus ConfigMap (DWD).
|
||
9. **Telegram** (Lieferung T4c), jeder Schritt mit Freigabe des Inhabers:
|
||
1. Der Inhaber legt bei BotFather einen Bot an (Gruppen aus, kein Webhook), je Umgebung einen eigenen.
|
||
2. Der Inhaber legt das Secret `apicore-telegram` mit `TELEGRAM_BOT_TOKEN` an. Die Komponente `backend-roles` bindet es nur in den Worker ein (`optional: true`); `TELEGRAM_BOT_USERNAME` kommt in die ConfigMap.
|
||
3. Deploy; „Verwaltung > System“ muss „Bot eingerichtet“ und einen frischen Abruf zeigen.
|
||
4. Der Inhaber verbindet sein Konto und prüft eine Test-Erinnerung (Aufgabe mit Fälligkeit heute).
|
||
- Ohne Secret bleibt Telegram aus; alles andere läuft.
|
||
10. **Webhook-Payload:** `inspection.created` trägt `feedStatus` künftig als `GOOD`/`WEAK`/`CRITICAL`. Der Shop nutzt dieses Ereignis nicht; im Changelog nennen.
|
||
11. **Frontend:** T3-Funktionen `varroaLight(rate, threshold)` und `INSPECTION_STALE_DAYS` werden ersetzt, nicht parallel gehalten.
|
||
12. **Doku:**
|
||
- Bestehende Annahmen ändern: 12 (Automatische Aufgaben → Vorschläge) und die T3-Annahme „Ampeln“ (Varroa-Bänder, Fälligkeit).
|
||
- Neue Annahmen jeweils unter der **nächsten freien Nummer** zum Zeitpunkt des Baus, keine fest vergebenen Nummern: Vorschläge und Anker; Saison aus Pflanzenentwicklung und Wetterquelle (Inhaber-Entscheidung Open-Meteo); Volksstärke in besetzten Waben mit Altwert; Bewegungsbuch der Honigcharge; Benachrichtigungen je Nutzer (Abweichung von Rückgrat-Spec §4); Erinnerungen und Telegram; Wassergehalt; Alter der Königin. Hintergrund: ARCHITECTURE hat 22 und 23 schon belegt, der T3-Plan plant 22–27.
|
||
- ARCHITECTURE §7 (Secret `apicore-telegram`, nur Worker), §8 (Backup-Ausnahmen) und §10 (T4-Handler, Job-Schritte, Worker-Schleifen) ergänzen. TASKS.md um T4.
|
||
13. **Cluster:** Jeder Schritt nur mit Freigabe des Inhabers, wie im Rückgrat-Runbook.
|
||
14. **Erste Woche:** Der Autopilot läuft scharf, ist aber in „Verwaltung > Automatisierung“ mit einem Schalter abschaltbar. Die Kalender-Rückfälle und die 8-Waben-Regel nach der ersten Saison mit den eigenen Meldungen prüfen.
|
||
|
||
## 16. Aufwand
|
||
|
||
| Baustein | PT |
|
||
|---|---|
|
||
| Vorschläge: Modell, Service, Regeln umstellen, Texte, API, Rechte-Filter | 2,0 |
|
||
| Ketten: Weiselzellen, Königin zugesetzt, Ableger, Volk beendet, Ereignisse `queens`/`colonies` | 1,5 |
|
||
| Varroa-Ampel nach Jahreszeit (Bänder, Endpunkt, Probe, UI) | 1,0 |
|
||
| Fälligkeit, Autopilot-Kern (Abgleich, Rangfolge, „Als Nächstes“, Heute, Vorschlagsliste) | 2,0 |
|
||
| Durchsicht → Volkskarte, Futter-Vokabular, Migration | 1,0 |
|
||
| Wartezeit-Kette: Prüfdienst, Warnungen, Meldungen, Kalender-Spanne | 1,0 |
|
||
| Benachrichtigungen je Nutzer | 0,5 |
|
||
| Pflanzenentwicklung: Wetter-Schleife, GTS, Meldungen, Kalibrierung, UI | 2,0 |
|
||
| Autopilot-Saisonregeln A4–A10 | 1,0 |
|
||
| Honig-Kette: Vorbelegung, Plausibilität, Ereignisse | 1,0 |
|
||
| Bewegungsbuch der Honigcharge inkl. Rückrechnung | 1,5 |
|
||
| Qualität vor der Abfüllung | 0,5 |
|
||
| Volksstärke in besetzten Waben: Feld, Migration, API, Oberfläche | 0,25 |
|
||
| Wetter-Adapter mit Konfiguration (`WeatherHistorySource`) | 0,25 |
|
||
| Erinnerungen in der App: Handler `notifications.task-due`, Job-Schritt `digest`, `NotificationPreference` mit API und Einstellungen-Karte | 1,0 |
|
||
| Telegram-Versand: Outbox `NotificationDelivery`, Schleife `telegram-send`, Rendern, Rechteprüfung beim Senden, Ruhezeit, API-Grenzen, Fehlerbehandlung | 1,0 |
|
||
| Telegram-Verbinden: Einmal-Token, Step-up, Empfang per Long Polling mit Lease, `/start`, `/stop`, `/hilfe` | 1,0 |
|
||
| Telegram-Oberfläche (`TelegramLinkCard`) | 0,5 |
|
||
| Telegram-Betrieb: Karte unter „Verwaltung > System“, System-API, Secret und Manifeste, Startprüfung | 0,5 |
|
||
| Doku, Gesamtprüfung | 0,5 |
|
||
| **Summe** | **20,0** |
|
||
|
||
Der Fahrplan nannte etwa 10 PT. Mehr wird es durch Punkte, die §10 nicht einpreist: das Bewegungsbuch der Honigcharge, den Wetterspeicher für die GTS, die Rechte-Filterung von Vorschlägen und Meldungen und den neuen Kanal Telegram (Inhaber-Antwort 3, 4,0 PT inklusive Erinnerungen).
|
||
|
||
**Vorschlag: drei Lieferungen.**
|
||
- **T4a (≈ 10,25 PT):** Vorschläge, Ketten, Varroa-Ampel, Fälligkeit und Autopilot-Kern mit Kalender-Rückfall (ohne GTS), Volkskarte mit besetzten Waben, Wartezeit, Volk beendet, Benachrichtigungen je Nutzer, Erinnerungen in der App. Bringt §30, die Vorschlagsliste und die Morgenübersicht.
|
||
- **T4b (≈ 6,75 PT):** Pflanzenentwicklung mit GTS, Wetter-Adapter und Meldungen, Saisonregeln A4–A10, Honig-Kette, Bewegungsbuch, Qualität.
|
||
- **T4c (≈ 3,0 PT):** Telegram. Braucht nur T4a; kann parallel zu T4b laufen.
|
||
|
||
## 17. Offene Fragen an den Inhaber
|
||
|
||
Keine. Die drei Fragen vom 2026-10-13 sind beantwortet und eingearbeitet (Abschnitt 1, „Antworten des Inhabers“):
|
||
|
||
- Volksstärke in besetzten Waben, 0–40, Altwerte bleiben (4.14).
|
||
- Freie Open-Meteo-API hinter einem Adapter (Entscheidungen, 6.3).
|
||
- Erinnerungen je Aufgabe und als Morgenübersicht, in der App und per Telegram (9a).
|
||
|
||
Werte, die der Inhaber nach der ersten Saison prüfen sollte, sind als „Annahme“ markiert und alle einstellbar: Kalender-Rückfälle der Phasen, Beginn der Schwarmzeit, 8-Waben-Regel, Ruhezeit 21:00–06:30, Altersschwelle der Königin.
|