Zum Inhalt

Unveröffentlicht

Änderungen, die noch nicht in einem Release veröffentlicht wurden.

Hinzugefügt

Backend

  • REQ-001 Stammdatenverwaltung: Botanische Familien, Arten, Sorten, Lebenszyklen (ArangoDB); Feld propagation_methods (13 Vermehrungsarten, Mehrfachauswahl) im Arten-Steckbrief — alle 143 Kulturpflanzen-Seed-Daten befüllt
  • REQ-002 Standortverwaltung: Sites, Locations (rekursive Hierarchie), Slots, Standorttypen
  • REQ-003 Phasensteuerung: Phasen-State-Machine (Keimung → Ernte), GDD/VPD/Photoperiod-Berechnung
  • REQ-004 Dünge-Logik: Düngemittel, Nährstoffpläne, Dosierungen, Mischsicherheit, Spülung, Runoff, EC-Budget, Wasserquelle/CalMag-Korrektur
  • REQ-006 Aufgabenplanung: Workflow-Templates, Tasks, Queue, Abhängigkeiten, HST-Validator
  • REQ-007 Erntemanagement: Ernte-Indikatoren, Beobachtungen, Batches, Qualitätsbewertung, Karenz-Gate
  • REQ-010 IPM-System: Schädlinge, Krankheiten, Behandlungen, Inspektionen, Resistenzmanager
  • REQ-011 Externe Stammdatenanreicherung: GBIF + Perenual Adapter, Enrichment-Engine, Celery-Tasks
  • REQ-012 Stammdaten-Import: CSV-Upload, Validierung, Vorschau, Bestätigung
  • REQ-013 Pflanzdurchlauf: PlantingRun, Batch-Operationen, State-Machine
  • REQ-014 Tankmanagement: Tanks, Tankzustände, Befüllungen, Wartung, Sensoren
  • REQ-015 Kalenderansicht: iCal-Feeds, Aggregation, Token-basierter Zugriff
  • REQ-019 Substratverwaltung: Erweiterte Substrattypen, Lebenszyklus-Manager, Wiederverwendung
  • REQ-020 Onboarding-Wizard: 5-Schritte-Assistent, 9 Starter-Kits, Erfahrungsstufen
  • REQ-022 Pflegeerinnerungen: 9 Pflegeprofile, FAMILY_CARE_MAP, adaptive Intervalle, Celery-Task
  • REQ-023 Authentifizierung: Lokale Konten (bcrypt), JWT (authlib), Refresh-Token-Rotation
  • REQ-024 Mandantenverwaltung: Tenant-Isolation, Mitgliedschaften, Einladungen, RBAC
  • REQ-028 Mischkultur & Companion Planting: Graph-basierte Kompatibilität
  • REQ-031 KI-Assistent: RAG-basierte Wissensdatenbank, LLM-Adapter (Anthropic/Ollama/OpenAI-kompatibel), Hybrid-Search
  • Mandanten exportieren und importieren (Issue #2138, REQ-056 §8): Ein Garten lässt sich jetzt als Archiv sichern und als neuer Garten wieder einspielen — in dieselbe oder eine andere Installation. Exportieren darf die Leitung mit Verwaltung (POST /api/v1/tenants/{slug}/exports, auch während der Löschfrist) oder ein Plattform-Admin (POST /api/v1/admin/platform/tenants/{key}/exports), jeweils mit Passwort-Bestätigung; höchstens ein Export läuft gleichzeitig (409 EXPORT_ALREADY_RUNNING) und höchstens TENANT_EXPORTS_PER_DAY (Standard 3) je 24 Stunden. Alle Mitglieder mit Verwaltung bekommen eine E-Mail. Das Archiv enthält alle Fachdaten, die Verbindungen dazwischen, die Fotos und die Mitglieder ohne E-Mail-Adresse — keine Anmeldedaten, API-Schlüssel, Einladungen, Integrations-Zugangsdaten oder Protokolle — und ist 72 Stunden ladbar. Importieren darf nur ein Plattform-Admin (POST /api/v1/admin/platform/tenant-imports): Jeder Datensatz bekommt eine neue Kennung, Mitglieder werden über die E-Mail-Adressen bestehender Konten zugeordnet, und ihre Rolle legt der Admin fest, nicht das Archiv. Ein Archiv mit fremden Datensätzen, fehlenden Katalogeinträgen oder einer beschädigten Datei lehnt der Import mit 422 ab, ohne etwas zu schreiben. Der neue Garten bleibt bis zum letzten Datensatz unerreichbar (Wird importiert), ein Fehler mitten im Import entfernt alles wieder, und ein abgebrochener Import wird stündlich aufgeräumt. Archive tragen die Signatur der Installation; der Import meldet mit verified, ob ein Archiv unverändert von hier stammt. Schlüssel von Integrationen bleiben draußen, und jeder Download steht im Sicherheitsprotokoll. Neu sind TENANT_EXPORTS_PER_DAY, TENANT_IMPORT_MAX_UPLOAD_MB und RATE_LIMIT_TENANT_IMPORT. Wie groß ein Archiv entpackt werden darf, ist fest begrenzt (1 GiB, davon höchstens 128 MiB Datensätze, dazu Kompressionsverhältnis und Datensatzzahl), unabhängig von TENANT_IMPORT_MAX_UPLOAD_MB. Der Eigentümer des neuen Gartens bekommt genau die Zusatzberechtigungen, die der Admin bestätigt, und deaktivierte Konten lassen sich nicht zuordnen. Auch eine einzelne riesige Zeile oder Millionen kleiner Werte lehnt der Import ab, bevor sie Speicher kosten; das Mitgliederlimit des Zielplans gilt auch für die importierten Mitglieder. Lässt sich ein gescheiterter Import nicht vollständig zurückrollen, bleibt der halbe Garten unerreichbar, der Import meldet cleanup_pending, und der stündliche Job löscht ihn. Auch abgelehnte und gescheiterte Importe stehen mit dem SHA-256 des Archivs im Sicherheitsprotokoll. Ein Archiv, das der Plan des neuen Gartens nicht fasst — mehr Mitglieder, mehr Speicher oder mehr Fotos je Pflanze —, lehnt der Import schon beim Antrag mit dem Code des Kontingents ab (MEMBER_LIMIT_REACHED, STORAGE_QUOTA_EXCEEDED, PHOTO_QUOTA_EXCEEDED), ohne etwas zu schreiben. Mit TimescaleDB nimmt das Archiv jetzt auch die Sensor-Messwerte des Gartens mit (timeseries/readings.csv: Rohwerte und Stunden-/Tageswerte, nur die des Gartens, gegen die Größengrenze des Exports gezählt und von der Signatur abgedeckt); das Archivformat bleibt kamerplanter-tenant-export/1, der Import übergeht die Datei und nennt im Bericht die Zahl der Zeilen. Neu ist TENANT_EXPORT_SIGNING_SECRET: ein eigenes Geheimnis für die Archiv-Signatur (mindestens 32 Zeichen; leer = wie bisher aus JWT_SECRET_KEY abgeleitet; im Helm-Chart optional über tenantExport.signingSecret). Der Import hat jetzt seine eigene Upload-Grenze TENANT_IMPORT_MAX_UPLOAD_MB statt der allgemeinen von 26 MiB — nur diese Route, höchstens bis UPLOAD_SPOOL_BUDGET_MB (mit den Standardwerten etwa 51 MiB); das Helm-Chart setzt dafür auf genau diesem Pfad die Grenze des Frontend-nginx und eines eigenen Ingress tenant-import (tenantExport.importMaxUploadMb). Wie du /tmp für größere Archive bemisst, steht im Runbook. Noch offen: das Einspielen der Messwerte. Die Oberfläche gibt es: Tab Export in den Garten-Einstellungen und die Seite Importe im Plattform-Modus (die Mitglieder-Schlüssel trägst du von Hand ein). Siehe Mandanten exportieren und importieren.
  • Pläne und Kontingente je Mandant (Issue #2142, REQ-056 §7): Jeder Mandant hat jetzt einen Plan (plan_key), der seine Kontingente festlegt — Mitglieder, Service Accounts, Speicher, Fotos je Pflanze und die drei KI-Tagesbudgets. Alle Mandanten starten mit dem Plan standard, bei dem jedes Kontingent der bisherigen Server-Einstellung folgt; ohne weitere Pläne ändert sich also keine Grenze, auch keine per Umgebungsvariable angepasste. Plattform-Admins legen Pläne über /api/v1/admin/platform/plans an, ändern und löschen sie und ordnen sie mit PATCH /api/v1/admin/platform/tenants/{key} (plan_key) zu; jede Änderung steht im Sicherheitsprotokoll. Ein niedrigeres Kontingent entfernt nichts, es lehnt erst die nächste Anlage ab. Jedes Mitglied liest Plan und Verbrauch über GET /api/v1/tenants/{slug}/entitlements. Die Ablehnungscodes bleiben dieselben. Neu ist TENANT_MAX_ORGANIZATIONS_PER_USER (Standard 10, 0 = Organisationen legt nur der Betreiber an): Die bisher fest eingebaute Grenze von zehn Organisationen je Konto lässt sich einstellen, die Ablehnung hat jetzt den Code ORGANIZATION_LIMIT_REACHED statt VALIDATION_ERROR (Status weiter 422), und eine Organisation in Löschung (deleted) zählt nicht mehr mit. Achtung: In einem Plan heißt 0 bei Speicher und Fotos „unbegrenzt“ und bei den KI-Budgets „Budget aus“; die Plan-Antwort nennt solche Kontingente im Feld unlimited. Jedes Kontingent hat eine Obergrenze, ein beschädigter Plan fällt auf die Server-Einstellungen zurück, und der Mandant platform folgt immer den Server-Einstellungen. Beim Start legt Kamerplanter den Plan standard an, und die Migration v0091 trägt ihn bei jedem bestehenden Mandanten ein.
  • Pläne und Kontingente in der Oberfläche (REQ-056 §15, AK-45): Plattform-Admins pflegen Pläne auf der neuen Seite Pläne (Einstellungen > Plattform-Modus > Pläne verwalten): Liste mit Standard-Markierung und Zahl der Mandanten, Formular zum Anlegen und Ändern, Löschen nur für ungenutzte Pläne. Ein leeres Kontingent steht als „wie Instanz-Einstellung“, ein 0 als „unbegrenzt“ bzw. „Budget aus“. Die Seite eines Mandanten zeigt Plan, Verbrauch je Kontingent und den Eigentümer und ordnet einen anderen Plan zu (nicht für den Mandanten platform). Jedes Mitglied sieht im neuen Tab Kontingente der Garten-Einstellungen Plan und Verbrauch seines Gartens. Die Ablehnungen ORGANIZATION_LIMIT_REACHED, MEMBER_LIMIT_REACHED, SERVICE_ACCOUNT_LIMIT_REACHED, STORAGE_QUOTA_EXCEEDED und PHOTO_QUOTA_EXCEEDED erscheinen als verständlicher Text auf Deutsch und Englisch statt der englischen Server-Meldung.
  • Datenschutz: eigene Einwilligung für die KI-Wissensfrage (REQ-031 v2.6, REQ-025 v1.35). Neuer optionaler Einwilligungszweck ai_knowledge_question („KI-Wissensfrage an die Wissensbasis“): Er erlaubt, deine frei formulierte Frage im KI-Assistenten an die Wissensbasis und ihr Sprachmodell zu senden. Pflanzenwerte gehen nur mit, wenn du sie selbst zur Frage angibst — dann ist zusätzlich ai_tenant_data_access nötig. Jederzeit widerrufbar, wirkt ab der nächsten Frage. Er erscheint in der Einwilligungsliste unter GET /api/v1/privacy/consents und wird über POST /api/v1/privacy/consents erteilt.
  • Datenschutz: eigene Einwilligung für den Schädlings-Erkennungsindex (REQ-025 v1.36, REQ-044 v1.4, REQ-010 v1.7). Neuer optionaler Einwilligungszweck pest_reference_contribution („Foto-Beitrag zur Schädlingserkennung“): Ein selbst beigetragenes Schädlingsfoto, das eine Administratorin oder ein Administrator für alle sichtbar freigibt, wird damit als Zahlenabdruck (Embedding) samt Herkunft in den gemeinsamen Schädlings-Erkennungsindex übernommen; das Foto verlässt die Installation nicht. Jederzeit widerrufbar, Wirkung ab der nächsten Freigabe. Er erscheint unter GET /api/v1/privacy/consents und im Tab Einwilligungen; erteilt wird er über POST /api/v1/privacy/consents (einen eigenen Schalter in der Oberfläche gibt es noch nicht).
  • API: CONSENT_REQUIRED nennt den fehlenden Zweck maschinenlesbar in details[0].purpose (zusätzlich, field bleibt consent) — eine Route kann aus zwei Zwecken ablehnen, und der Client muss nicht mehr die Meldung auslesen.
  • API: Cloud-Kennzeichnung der Wissensfrage. Die Antwort von POST /api/v1/t/{slug}/ai/knowledge/ask trägt zusätzlich provider_type und uses_cloud_provider, eingestuft wie beim Glossar nach dem System-Standard-Provider der Plattform (der Knowledge Service antwortet mit seinem eigenen Modell).
  • Einladungslink für mehrere Personen (Issue #2182, REQ-024 v1.34 AK-07): Beim Einladungslink legst du unter „Personen pro Link“ fest, wie viele Personen mit ihm beitreten können (1–100, voreingestellt 1 wie bisher). Jede Nutzung wird atomar gezählt, die Liste der Einladungen zeigt „Genutzt“ (z. B. „3 von 20“, auf dem Handy als Karte), und jede Nutzung zählt gegen das Mitgliederlimit. API: POST /api/v1/tenants/{slug}/invitations/link nimmt max_uses an, die Einladungsliste trägt max_uses und use_count. Scheitert ein Beitritt, wird die Nutzung zurückgegeben — eine inzwischen widerrufene Einladung bleibt dabei widerrufen. Eine Einladung in einen gesperrten, verwaisten oder zur Löschung vorgemerkten Garten lässt sich nicht annehmen (422); eine Nutzung wird nach einem gescheiterten Beitritt nur zurückgegeben, wenn die Mitgliedschaft nachweislich nicht entstanden ist.
  • OIDC-Auto-Join (Issue #2182, REQ-024 v1.34 AK-09, REQ-023 v1.49): Hat ein Anmeldeanbieter einen Standard-Mandanten (default_tenant_key), tritt ein Konto, das beim ersten Anmelden über ihn neu entsteht, diesem Mandanten als Beobachter bei. Bisher war das Feld gespeichert, wirkte aber nicht. Nie in den Mandanten platform oder einen persönlichen Garten; ein unbekannter, nicht aktiver, gelöschter oder voller Mandant wird still übersprungen, die Anmeldung gelingt. Der Beitritt steht im Sicherheitsprotokoll (via: oidc_auto_join). Nur bei einem generischen OIDC-Anbieter (bei Google, GitHub und Apple kann jede Person ein Konto anlegen) und nur für ein Konto, dessen E-Mail-Adresse der Anbieter als bestätigt meldet. Den Standard-Mandanten prüft Kamerplanter beim Speichern (Mandant vorhanden, kein persönlicher, nicht platform; sonst 422), die Provider-Antwort zeigt ihn, und null oder "" entfernt ihn. Scheitert der Beitritt unerwartet, bleibt die Anmeldung davon unberührt.
  • Dokumentation (Issue #2179): Die Anwenderdoku nannte eine Plattform-Rolle viewer mit Lesezugriff auf den Admin-Bereich. Diese Rolle gibt es nicht; sie ist jetzt als „Noch nicht implementiert“ gekennzeichnet (REQ-024 §1a.4).
  • Upgrade vom Light- in den Full-Modus (Issue #1855, REQ-027 v1.9 §7a): Jeder Start merkt sich den Betriebsmodus (Collection system_meta). Startet eine Light-Installation zum ersten Mal mit KAMERPLANTER_MODE=full, sperrt Kamerplanter das System-Konto des Light-Modus (is_active=false), macht seine API-Schlüssel ungültig, beendet seine Sitzungen und löscht seine Kalender-Abos. Ein im Light-Modus ohne Anmeldung erzeugter kp_-Schlüssel bekommt danach 401 (AK-35); Home Assistant und MCP-Clients brauchen einen neuen Schlüssel. Der Garten des Light-Modus bleibt unverändert. Neu sind GET /api/v1/system/takeover-status und POST /api/v1/system/takeover (nur im Full-Modus): Das erste registrierte Konto — oder ein schon vorhandener Plattform-Admin — übernimmt den Garten (accept: true, es wird Leitung mit beiden Verwaltungsrechten, die Daten bleiben, wo sie sind) oder lehnt ab; eine Instanz ohne Plattform-Admin macht es dabei zum ersten Plattform-Admin. Startet die Instanz wieder im Light-Modus, entsperrt Kamerplanter das System-Konto und gibt ihm seinen Garten zurück. Jede Änderung steht im Sicherheitsprotokoll (via: mode_switch / light_mode_takeover, neue Aktion tenant_taken_over). Nach dem Security-Review: Übernehmen und Ablehnen verlangen den Step-up des Kontos (neue Step-up-Aktion light_mode_takeover); ohne Plattform-Admin entscheidet nur das erste nach dem Umstieg registrierte aktive Konto mit bestätigter Adresse und eigenem Garten; die Übernahme wird beansprucht und erst nach Erfolg entfernt, ein Fehler mittendrin wird zurückgenommen; der Full-Modus meldet das System-Konto unabhängig von der Datenbank nie an (auch nicht über MCP); neue Audit-Aktionen takeover_accepted, takeover_declined, takeover_reverted, system_account_sealed; ein Light-Start mit anderen Konten schreibt eine critical-Warnung. Nach der zweiten Review-Runde: Scheitert die Übernahme mittendrin (auch beim Protokollieren oder beim Plattform-Admin-Recht), nimmt Kamerplanter alles bereits Geschriebene zurück; gelingt auch das nicht, merkt es sich den Rest an der offenen Übernahme (needs_attention, Audit takeover_undo_failed) und nimmt ihn zurück, bevor jemand neu entscheidet. Abgewiesene Versuche stehen als takeover_refused im Sicherheitsprotokoll; einen Bestätigungscode bekommt nur, wer entscheiden darf. Nach der dritten Runde: Die gespeicherte Restquittung enthält das entscheidende Konto nicht mehr, das Zurücknehmen prüft vor jedem Schritt, ob die Entscheidung noch gilt, ein inzwischen gelöschter Garten blockiert die Übernahme nicht mehr dauerhaft, die Kontolöschung nimmt den Light-Garten nicht mit, solange eine Restquittung besteht, und beide Übernahme-Endpunkte sind auf 10 Aufrufe pro Minute und Konto begrenzt (RATE_LIMIT_TAKEOVER).

Frontend

  • Übernahme-Dialog nach dem Upgrade auf Full (Issue #1855, REQ-027 Szenario ⅚): Nach dem Wechsel vom Light- in den Full-Modus fragt ein Dialog das erste Konto, ob es den bisherigen Garten übernehmen möchte, und zeigt, was darin liegt (Pflanzen, Standorte, Durchläufe, Nährstoffpläne, Aufgaben). „Ja, übernehmen“ wechselt direkt in den Garten, „Nein, neu starten“ lässt ihn für den Admin-Bereich stehen, „Später entscheiden“ fragt bei der nächsten Anmeldung wieder. Die Entscheidung bestätigst du mit deinem Passwort (oder der erneuten Anmeldung beim Anbieter bzw. einem Code).
  • Alle REQ-001 bis REQ-024 Frontend-Seiten implementiert
  • REQ-020 Onboarding-Wizard (5-Step MUI Stepper)
  • REQ-021 UI-Erfahrungsstufen: Feldkonfiguration, Navigation-Tiering, ExperienceLevelSwitcher
  • REQ-022 Pflege-Dashboard mit Urgency-Gruppierung
  • Light/Dark Theme mit localStorage-Persistenz
  • i18n Deutsch/Englisch (react-i18next)

Infrastruktur

  • CI (Entwicklung): mypy-Ratchet für das Backend (Issue #2169). strict = true in src/backend/pyproject.toml lief bisher in keiner Prüfung — gemessen 3452 Befunde in src/backend/app, darunter seit Juli der [call-arg]-Fehler, hinter dem #2150 stand. task typecheck:backend (Schritt „mypy ratchet“ im Backend-CI-Job lint-test, lokal zusätzlich als pre-commit-Hook) vergleicht die Befunde je Datei und Fehlercode mit src/backend/mypy-ratchet-baseline.json: Ein neuer Befund ist rot, eine neue Datei muss fehlerfrei sein. Für call-arg, name-defined, attr-defined und call-overload muss die Zahl genau stimmen — behebst du einen solchen Befund, senkst du den Eintrag im selben Change mit task typecheck:backend -- --tighten. Der Lauf ist advisory, solange lint-test kein Pflicht-Check ist.
  • Metriken (Prometheus): Das Backend liefert http_request_duration_seconds, http_requests_total, http_requests_in_progress und Prozess-Metriken auf einem eigenen Port aus (METRICS_PORT, Standard 0 = aus; Issue #2129, MT-033, NFR-007 v1.1). Im Helm-Chart schaltet monitoring.enabled: true den Port 9464, einen ServiceMonitor (Prometheus Operator) und die NetworkPolicy backend-metrics ein, die nur die konfigurierten Prometheus-Pods auf diesen Port lässt. /metrics ist keine Route der API und über den Ingress nicht erreichbar. Labels: method, handler (Routenmuster, nie der Pfad), status — kein Mandant, keine Person. Neue Abhängigkeit prometheus-client (Apache-2.0, bündelt decorator unter BSD-2-Clause; Lizenz-Positivliste ergänzt).

  • MkDocs-Dokumentationsinfrastruktur mit Material Theme und DE/EN i18n (NFR-005)

  • ADR-001 bis ADR-006: Architektur-Entscheidungen dokumentiert
  • Skaffold-basierter Entwicklungsworkflow mit Kubernetes/Helm
  • pgvector + Embedding-Service für RAG-Pipeline
  • Knowledge-Container für YAML-basierte Wissensbasis
  • GitHub Actions CI/CD (Docker Lint/Build, Skaffold Verify)

Geändert

Dokumentation

  • Light-Modus und KI: Die Spezifikation versprach, dass das Backend im Light-Modus nicht mit einem Cloud-Sprachmodell startet. Diese Prüfung gibt es nicht, und in der beschriebenen Form hätte sie nichts bewirkt — das Sprachmodell wählt der Knowledge Service über LLM_PROVIDER. Die Seite Light-Modus sagt jetzt, dass du für den Light-Modus am Knowledge Service ein lokales Modell einstellen musst (Issue #2176).
  • Rollen und Berechtigungen, Plattform-Admin: Die Seiten sagen jetzt, was gilt — eine Standort-Zuweisung schränkt das Bearbeiten nicht ein, das erste registrierte Konto wird nicht automatisch Plattform-Administrator (eingerichtet wird es auf dem Server), ein Plattform-Administrator sieht keine Pflanzen oder Ernten fremder Gärten, und der geführte Umstieg vom Light-Modus ist als noch nicht gebaut gekennzeichnet.
  • Mandanten & Gärten: Der Rollenvergleich nennt die aktuellen Rollen Leitung, Gärtner und Beobachter statt der abgeschafften Rolle „Admin“; Einladen, Rollen ändern und Einstellungen gehören zur Zusatzberechtigung Verwaltung. Eine Einladung vergibt die Rolle Beobachter (Issue #2181).
  • Spezifikation, Auswahlwerte: 20 Wertelisten in REQ-001, -002, -004, -006, -007, -014 und -019 nannten andere Werte, als die Anwendung speichert — etwa Standorttyp ohne Fensterbank, Balkon und Growzelt, Erntequalität A+ statt des gespeicherten a_plus (angezeigt wird weiterhin „A+“), Frostempfindlichkeit tender statt sensitive, Nährstoffphase harvest statt ripening. Die Spezifikation nennt jetzt die Werte der Anwendung; an der Anwendung selbst ändert sich nichts. Eine automatische Prüfung vergleicht diese Wertelisten künftig mit dem Code (Issue #2183).
  • Spezifikation, Mandanten-Lebenszyklus und Kontingente (Issue #2143): Neue Anforderung REQ-056 führt Zustände, Sperre, Registrierung, Kontingente, Mandanten-Export, Eigentum und Ansprechperson eines Mandanten an einem Ort zusammen. Geplant und noch nicht umgesetzt sind Pläne mit eigenen Kontingenten (Standardplan standard mit den heutigen Grenzen), ein Export des ganzen Mandanten mit Import in eine andere Installation, die Übergabe der Verantwortung für einen Garten und eine Ansprechperson für den Betreiber; Preise und Abrechnung sind ausdrücklich nicht Teil davon. Beim Messen fiel auf: Eine Sperre lässt sich umgehen, wenn ein Plattform-Admin einen gesperrten Mandanten löscht und dessen Verwaltung die Löschung abbricht (der Mandant ist danach wieder aktiv), und sechs automatische Abläufe — Tank-Warnungen, Home-Assistant-Messwerte, Wetterdaten, InvenTree-Abgleich (zwei) und der Benachrichtigungs-Digest — laufen für einen gesperrten Mandanten weiter. Die Admin-Anleitung behauptete, Kontingente und Limits eines Mandanten ließen sich einsehen — das ist als „Noch nicht implementiert“ gekennzeichnet. NFR-012 hat eine Kontingent-Tabelle (§10.3), auf die NFR-013 bisher ins Leere verwies.

Frontend

  • Garten-Einstellungen: geplante Löschung abbrechen (Issue #2166). Ein Garten, dessen Löschung geplant ist, war für seine Mitglieder unsichtbar — er fehlte im Tenant-Selektor, und den Abbruch gab es nur für Plattform-Admins. Die Garten-Einstellungen — jetzt über den Tenant-Selektor mit Garten-Einstellungen erreichbar — zeigen einen Abschnitt Zur Löschung vorgemerkte Gärten mit Zustand und Löschdatum. Hast du in so einem Garten die Rolle Leitung und die Zusatzberechtigung Verwaltung, brichst du die Löschung dort mit Löschung abbrechen ab (Bestätigung mit deinem eigenen Passwort, einem E-Mail-Code oder einer erneuten Anmeldung); der Garten ist danach wieder aktiv und im Tenant-Selektor. Ein verwaister Garten wird angezeigt, lässt sich aber nicht zurückholen.
  • Kalender, iCal-Feeds (Issue #2171): Die Abo-URL eines Feeds zeigt Kamerplanter nur noch einmal, direkt nach dem Anlegen oder nach dem Erneuern des Tokens, in einem eigenen Fenster mit URL kopieren und dem Hinweis, dass sie danach nicht mehr abrufbar ist. Die Feed-Liste zeigt keine URL und keinen Kopieren-Knopf mehr, sondern den Hinweis, dass du für eine neue URL den Token erneuerst. Token erneuern fragt jetzt vorher nach, weil die alte URL danach in keiner Kalender-App mehr funktioniert. Ein Klick neben das URL-Fenster schließt es nicht mehr; lässt der Browser das Kopieren nicht zu, sagt dir Kamerplanter, dass du die URL selbst aus dem Feld kopierst. Schlägt das Anlegen fehl, bleibt das Fenster mit deinem Namen offen, und Erstellen ist gesperrt, solange die Anfrage läuft — ein Doppelklick legt keinen zweiten Feed an.
  • Datenschutz (Verhaltensänderung): Das Frontend meldet Fehler nur noch nach Einwilligung (Issue #2159, UI-NFR-013 CI-001/CW-003). Bisher startete der Browser das Fehler-Tracking-SDK, sobald eine SENTRY_DSN gesetzt war, und das Einwilligungs-Banner war nirgends eingebunden. Jetzt erscheint bei gesetzter DSN beim ersten Besuch das Banner; das SDK wird erst nach Zustimmung zur Fehleranalyse geladen. Unter Datenschutz → Einwilligungen schaltet „Fehleranalyse erlauben" das Melden ohne Neuladen ab oder an, auch in anderen Tabs. Im Light-Modus erscheint kein Banner und das Frontend meldet keine Fehler. Ohne DSN ändert sich nichts. Für Betreiber: Frontend-Fehler kommen nur noch von Personen, die zugestimmt haben.
  • KI-Assistent (Verhaltensänderung): Die Seite KI-Assistent stellt Wissensfragen im Voll-Modus über deinen Garten (/t/{slug}/ai/knowledge/ask) statt über den öffentlichen Pfad, den es dort nicht mehr gibt; im Light-Modus bleibt es bei /public/ai/ask (Issue #2175). Fehlt die Einwilligung „KI-Wissensfrage an die Wissensbasis“, bietet die Seite sie direkt an und sendet die Frage nach dem Einwilligen erneut. Ein ausgeschalteter KI-Schalter des Gartens und ein aufgebrauchtes Tageskontingent werden verständlich erklärt. Antwortet die Wissensbasis über ein Cloud-Modell, trägt die Antwort die Kennzeichnung Cloud-Verarbeitung wie die übrigen KI-Antworten. Welche Einwilligung fehlt, liest die Seite aus details[0].purpose; nur bei älteren Servern ohne dieses Feld aus dem Meldungstext.
  • Pflanzenerkennung (Verhaltensänderung): Der Schalter Als Referenzbild für die Erkennung nutzen ist jetzt die Einwilligung „Foto-Beitrag zur Pflanzenerkennung“ (Issue #2174). Eingeschaltet erklärt er, was geteilt wird; beim Speichern wird die Einwilligung erteilt, bevor das Foto beigetragen wird. Schlägt das fehl, wird die Pflanze nicht angelegt und ein Fehler angezeigt, statt den Beitrag still zu überspringen. Im Light-Modus ist der Schalter ausgeblendet.
  • Datenschutz: Im Tab Einwilligungen lässt sich jede erteilte optionale Einwilligung mit Widerrufen zurücknehmen; bisher ging das nur über die API. Der Link „Datenschutz-Einstellungen“ im Einwilligungsdialog der Pflanzenerkennung führt jetzt auf die Datenschutz-Seite statt auf die Kontoeinstellungen.
  • Glossar: Die Schaltfläche Ausführliche Erklärung erzeugen verschwindet, sobald die KI-Funktionen für den Garten oder die Installation ausgeschaltet sind; an ihrer Stelle steht der Grund statt eines Erneut-Versuchen-Knopfs, der genauso abgelehnt würde.
  • Einstellungen: Den Reiter „Plattform-Modus" sehen nur noch Plattform-Admins; für alle anderen fragt die Seite keine Admin-Daten mehr ab (Issue #2144).
  • Admin, Einladungen, KI-Assistent, Aufgaben einer Pflanze und Lagerbeobachtungen laden ihre Listen seitenweise vollständig, seit die Schnittstelle dort nur noch 50 Einträge pro Seite liefert (Issue #2131) — du siehst weiterhin alle Einträge.
  • Mitglieder und Standort-Zuweisungen eines Gartens, die Mitgliederliste in der Mandantenverwaltung, die Durchläufe einer Workflow-Vorlage und die InvenTree-Verknüpfungen laden ebenfalls seitenweise vollständig (Issue #2131) — du siehst weiterhin alle Einträge.
  • Datenschutz: Der Dialog zum Löschen des Kontos nennt vor der Bestätigung die Organisationen, in denen die Verwaltung an die dienstälteste Leitung übergeht oder die verwaisen (Issue #2134)
  • Admin: Die Mandantenverwaltung zeigt den Zustand eines Mandanten (aktiv, gesperrt, Löschung geplant, verwaist, wird gelöscht) samt Löschdatum und bietet bei geplanter Löschung Löschung abbrechen mit Passwortbestätigung; Aktiv-Schalter und Löschen sind währenddessen gesperrt (Issue #2123)
  • Abmelden — und das Abmelden, weil die Sitzung anderswo beendet wurde — leert jetzt alles, was der Browser für das Konto hielt: jede geladene Liste, den aktiven Garten (kp_active_tenant_slug) und die Browser-Push-Registrierung dieses Geräts (Issue #2117, MT-020). Auf einem gemeinsam genutzten Gerät sieht die nächste Person nichts mehr von den Daten des vorherigen Kontos und erhält keine seiner Push-Nachrichten. Ein Abmelden, dessen Anfrage scheitert (offline), meldet den Tab trotzdem ab. Reste aus dem Light-Modus im Browser (lokale Favoriten, Modul-Sichtbarkeit, Dashboard-Layout) übernimmt nur das erste Konto, das sie auf diesem Browser übernimmt.
  • Antwortet eine Anfrage erst, nachdem du dich abgemeldet hast (oder abgemeldet wurdest), landet ihr Ergebnis nicht mehr im Tab (Issue #2253): Eine langsame Liste legt die Einträge des vorherigen Kontos nicht zurück, ein spät geladenes Gartenverzeichnis stellt dessen aktiven Garten nicht wieder her, und ein spät geladenes Profil meldet den Tab nicht wieder als dieses Konto an. Anfragen, die nach dem Abmelden starten, sind davon nicht betroffen.
  • Admin: Auf der Seite eines Mandanten gibt es den Abschnitt Home-Assistant-Entitäten — Inventur der Instanz mit Suche, Schalter je Entität und Freigabe per ID für Benachrichtigungsdienste (Issue #2112). Sensor- und Wetterquellen-Dialog fragen die Home-Assistant-Liste nur noch mit dem Recht Technik ab und erklären sonst, warum sie fehlt; ohne Freigaben weist der Sensor-Dialog auf den Plattform-Admin hin.
  • Anmeldung und Registrierung folgen dem Registrierungsmodus (Issue #2132): Ist die Registrierung geschlossen, fehlt der Registrieren-Link und die Registrierungsseite erklärt es; ist sie nur mit Einladung möglich, fragt die Seite nach dem Einladungscode (/register?invitation=<code> füllt ihn vor). Eine abgelehnte Anmeldung über einen OIDC-Anbieter erklärt den Grund.
  • Einladungen: Ist ein Garten voll, erklärt die Seite zum Annehmen einer Einladung das in deiner Sprache — und dass die Einladung gültig bleibt, bis ein Platz frei wird (Issue #2133)
  • Benachrichtigungen: Gespeicherte Apprise-URLs erscheinen maskiert (tgram://****#1), ein Hinweis unter dem Feld erklärt, wie du sie behältst, entfernst oder ergänzt; eine neue Zeile lässt sich jetzt auch per Enter beginnen (Issue #2113).
  • Admin: Nach dem Löschen einer Organisation meldet die Oberfläche „Löschung angenommen“ statt „Organisation gelöscht“ — die Löschung läuft im Hintergrund (Issue #1792)
  • Pflanzinstanzen werden überall mit einem sprechenden Namen angezeigt (z. B. BASIL-001 (Basilikum – Genovese)) statt nur der technischen Instanz-ID; die Instanz-ID bleibt als sekundäre Information erhalten
  • Aufgaben: Die Formulare in den Tabs Bearbeiten und Abschließen zeigen Validierungsfehler jetzt als Hinweistext direkt am betroffenen Feld statt als kurzlebige Browser-Sprechblase (noValidate)
  • Konto: Die Seite zur E-Mail-Bestätigung bietet auch im Fehlerfall (ungültiger oder abgelaufener Link) einen Anmelden-Button — vorher war diese Seite eine Sackgasse

Backend

  • Schnellere Aufgaben-Warteschlange bei vielen Gärten (Issue #1534): Warteschlange, überfällige Aufgaben und die nach Status gefilterte Aufgabenliste eines Gartens lesen jetzt nur noch die Aufgaben dieses Gartens. Bisher las jede davon alle offenen Aufgaben der ganzen Installation — gemessen bei rund einer Million Aufgaben in 3 400 Gärten etwa 1,4 s je Abfrage für einen kleinen Garten, jetzt rund 2 ms. Den neuen Datenbank-Index legt Kamerplanter beim nächsten Start an (Migration v0092); für dich ändert sich sonst nichts.
  • Behoben (Sicherheit): Upload-Grenze greift vor dem Einlesen, Anhänge kommen nicht mehr ungefragt aus dem Browser-Cache (Issue #2254, NFR-013 v1.13). FastAPI liest einen Upload vollständig ein — Dateien über 1 MiB nach /tmp —, bevor Anmeldung oder Größenprüfung laufen. Gemessen: Ein 50-MiB-Upload gegen eine Grenze von 1 MiB wurde bis zum letzten Byte gelesen, auch ohne Anmeldung. Jetzt prüft das Backend die Größe vorher: Jeder Upload ist auf STORAGE_MAX_FILE_SIZE_MB + 1 MiB begrenzt (dieselbe Grenze wie der Frontend-nginx). Mit größerer Content-Length antwortet es 413 (REQUEST_BODY_TOO_LARGE), ohne ein Byte zu lesen; ein Body ohne Längenangabe wird an der Grenze abgebrochen. Gleichzeitige Uploads teilen sich ein Budget für /tmp (UPLOAD_SPOOL_BUDGET_MB, Standard 52, gezählt in angekommenen Bytes); wer nach UPLOAD_SPOOL_WAIT_SECONDS (30) keinen Platz bekommt, erhält 503 mit Retry-After. Downloads per Token-Link (/api/v1/attachments/token/…) tragen Cache-Control: no-store statt private, max-age=86400: Der Link gilt 15 Minuten, die Kopie im Browser hätte einen Tag gehalten, auch nach dem Löschen des Anhangs. Anhänge und Vorschaubilder in der App tragen private, no-cache mit ETag: Der Browser fragt vor jeder Anzeige nach und bekommt 304, wenn sich nichts geändert hat und du den Anhang noch sehen darfst. Abmelden, entzogene Mitgliedschaft und Löschen wirken so sofort. Der CSV-Import, die Schädlingserkennung und die Pflanzenbestimmung lesen höchstens ein Byte über ihre Grenze ein. Alle anderen Requests (JSON, Formulare) dürfen höchstens 1 MiB groß sein. Freigegebene Schädlingsbilder tragen ebenfalls private, no-cache statt public, max-age=86400, und das ETag aller Bild-Downloads verrät nicht mehr den Hash der hochgeladenen Originaldatei. Ein zu großes Foto in der Schädlingserkennung antwortet 413 statt 422, sodass die App wieder ihre Meldung „Bild zu groß“ zeigt. Eine Content-Length wie ² führte vorher zu einem 500.
  • Behoben (Sicherheit): Home Assistant, InvenTree und Web-Push lassen sich nicht mehr auf interne Adressen umlenken (Issue #2249). Die SSRF-Prüfung löste den Hostnamen auf und prüfte die Adressen; beim Verbindungsaufbau löste der HTTP-Client den Namen aber erneut auf. Ein Name, der der Prüfung eine öffentliche und der Verbindung eine interne Adresse liefert (DNS-Rebinding), erreichte so 127.0.0.1 oder Cluster-Dienste — bei InvenTree setzt die technische Verwaltung jedes Mandanten die URL selbst, auch mit http. Jetzt prüft die Verbindung selbst die Adressen, zu denen sie aufgebaut wird, nach derselben Regel, und verbindet sich nur mit einer geprüften Adresse; Host-Header, TLS-SNI und Zertifikatsprüfung bleiben beim Hostnamen. Das gilt für jeden Aufruf von Home Assistant (auch „Verbindung testen“) und InvenTree. Web-Push folgte einer Weiterleitung (301–308) des Push-Dienstes zu jedem beliebigen Ziel, ungeprüft; jetzt wird keiner Weiterleitung mehr gefolgt, die Zustellung zählt als fehlgeschlagen. Ein Web-Push-Aufruf hat außerdem ein Zeitlimit von 10 s (bisher keins). Eine fehlerhafte URL wie http://[::1 lehnen die SSRF-Prüfungen jetzt mit 422 ab statt mit einem Serverfehler. An legitimen Verbindungen ändert sich nichts; die Umgebungsvariablen HTTP(S)_PROXY gelten für Home Assistant und InvenTree nicht mehr, weil ein Proxy den Namen selbst auflösen würde. Achtung: Für Home Assistant und InvenTree gelten jetzt auch CGNAT-Adressen (100.64.0.0/10, etwa Tailscale) und IPv6-Site-Local (fec0::/10) als privat und brauchen HA_ALLOW_PRIVATE_ENDPOINT bzw. INVENTREE_ALLOW_PRIVATE_ENDPOINT; eine in IPv6 eingebettete IPv4-Adresse (6to4, NAT64, IPv4-mapped) wird wie diese IPv4-Adresse beurteilt, und die Metadaten-Adressen 100.100.100.200 und fd00:ec2::/32 sind immer gesperrt. Web-Push: verbleibendes Risiko. Für Web-Push schließt #2249 nur die Weiterleitungen. Der Push-Endpunkt wird vor dem Senden geprüft, beim Verbinden aber erneut aufgelöst; ein umgebogener Name endet am TLS-Zertifikat, Kamerplanter öffnet dabei aber eine TCP-Verbindung zur internen Adresse. Der Fehlertext einer fehlgeschlagenen Zustellung nennt deshalb nur noch delivery failed statt des Ausnahmetyps.
  • Behoben (Sicherheit): Ein Löschabbruch hebt eine Sperre nicht mehr auf (REQ-056 §4.3, REQ-024 v1.36 AK-65). Plante ein Platform-Admin die Löschung eines gesperrten Mandanten, konnte dessen eigene Verwaltung über POST /api/v1/tenants/{slug}/erasure/cancel abbrechen — und der Mandant war danach active, die Sperre also umgangen. Jetzt merkt sich das Planen der Löschung den bisherigen Zustand (status_before_deletion), und der Abbruch stellt ihn her: Ein gesperrter Mandant bleibt gesperrt, die Verwaltung darf die Löschung aber weiterhin abbrechen und so den Datenverlust verhindern. Eine vor dieser Änderung geplante Löschung weiß nicht, ob sie eine Sperre eingefroren hat: Sie kann nur ein Platform-Admin abbrechen (danach active); die Verwaltung erhält 403 (keine Migration). Alle Zustandswechsel (Sperren, Reaktivieren, Löschung planen, abbrechen) sind jetzt bedingte Schreibvorgänge: Hat sich der Zustand zwischen Lesen und Schreiben geändert, antwortet die Anfrage mit 409 und ändert nichts. Eine wiederholte Löschanfrage überschreibt den gemerkten Zustand nicht mehr und macht eine verwaiste Organisation nicht mehr abbrechbar. Die Löschung eines gesperrten Mandanten kann nur ein Platform-Admin planen (403 für jeden anderen Weg, gleiche Meldung). Nebenbefund: Nach einem Abbruch blieb das Löschdatum (deletion_scheduled_at) am wieder geöffneten Mandanten gespeichert, weil das Mandanten-Repository ein auf null gesetztes Feld nicht schrieb; ein explizites null entfernt jetzt das Feld — nur bei deletion_scheduled_at, status_before_deletion und description, jedes andere Feld lehnt das Repository ab.
  • Behoben (Sicherheit): Hintergrundjobs lassen gesperrte und zur Löschung vorgemerkte Mandanten aus (REQ-056 §5.2). 13 periodische Aufgaben fragten den Mandantenzustand nicht und arbeiteten für einen gesperrten oder zur Löschung vorgemerkten Mandanten weiter: Tank-Warnprüfung, E-Mail-Tagesbericht (stellte auch Benachrichtigungen über den Mandanten aus der Zeit vor der Sperre zu), Home-Assistant-Messwerte, Wetter, Klimanormalwerte, Winterhärtezonen, Bewässerungsbedarf, beide Saison-Läufe, beide InvenTree-Synchronisationen, Aktor-Regelkreis und Aktor-Statusabgleich. Alle fragen jetzt ActiveTenants. Jeder Eintrag im beat_schedule ist in TENANT_WRITING_BEATS oder TENANT_AGNOSTIC_BEATS (app/tasks/tenant_gate.py, mit Begründung) eingeordnet; der Guard tests/unit/guards/test_beats_skip_inactive_tenants.py schlägt bei einem neuen, nicht eingeordneten oder nicht gesperrten Beat fehl. Aufbewahrung, Löschung, Speicher-Abgleich und globale Katalogjobs laufen weiter. Aktoren eines geschlossenen Mandanten bleiben nicht hängen: Für einen gesperrten, zur Löschung vorgemerkten oder verwaisten Mandanten laufen keine Zeitpläne, Übersteuerungen und Sensorregeln mehr; der Regelkreis fährt seine Geräte stattdessen in einen sicheren Zustand. Sicherheitsregeln greifen weiter; sonst wird jedes Gerät, das ein Notfall-Szenario ausschaltet (Ventile, Pumpen, Heizung, Licht, CO₂ …), einmal ausgeschaltet — der Abluftventilator läuft weiter, und fail_safe_state am Gerät geht vor. Manuelle Aktoren bleiben unberührt, und in den geschlossenen Mandanten werden keine Ersatzaufgaben geschrieben.
  • Behoben (API): Die Mitgliederliste meldet die Zusatzberechtigungen (Issue #2166). GET /api/v1/tenants/{slug}/members lieferte für jedes Mitglied admin_scopes: [], auch für Mitglieder mit Verwaltung oder Technik — die Datenbankabfrage las das Feld nicht. Jetzt stehen die gespeicherten Zusatzberechtigungen in der Antwort. Neu (API): GET /api/v1/tenants?include_scheduled_deletion=true listet zusätzlich die Mandanten in pending_deletion und orphaned; jeder Eintrag trägt jetzt status und deletion_scheduled_at. Ohne den Parameter bleibt die Liste wie bisher (nur aktive Mandanten).
  • Behoben: Der Test einer noch nicht gespeicherten Wetterquelle (POST /api/v1/t/{slug}/sites/{key}/weather-sources/test) antwortete mit 500, wenn kind nicht zur Quelle passte (etwa dwd als home_assistant). Jetzt kommt reachable: false mit einer Meldung, die den Widerspruch nennt (Issue #2169).
  • Behoben (Datenschutz, Issue #2166): Beantragt die letzte Person mit Verwaltungsrecht einer Organisation die Löschung ihres Kontos, wird die Organisation jetzt beim Antrag geregelt — die dienstälteste Leitung übernimmt, oder die Organisation verwaist und wird nach der Frist gelöscht — und die Mitglieder bekommen sofort eine E-Mail. Bisher geschah das erst beim endgültigen Löschen bis zu 90 Tage später; so lange konnte niemand die Organisation verwalten, weil das Konto ab dem Antrag geschlossen ist. Die Frist einer verwaisten Organisation läuft ab dem Antrag. Ein Kontolöschantrag lässt sich nicht zurücknehmen, deshalb wird auch die Übergabe nicht zurückgenommen (REQ-025 v1.37 §3.1.3 Regel 4).
  • Behoben (Mandanten, Issue #2166): Die täglichen Erinnerungs- und Aufgaben-Beats (Pflege-Erinnerungen, Pflege-Benachrichtigung, Tageszusammenfassung, Eskalation, Tank-Wartung, Runoff-Spülung, Frostwarnung, Gieß- und Düngeaufgaben, Home-Assistant-Abgleich der Tanks) lassen einen Mandanten aus, der gesperrt, zur Löschung vorgemerkt oder verwaist ist. Bisher erzeugten sie dort weiter Aufgaben, lasen seine Home-Assistant-Sensoren und benachrichtigten die Mitglieder über einen Garten, den sie nicht öffnen können (REQ-024 v1.32 AK-65).
  • Behoben (Mandanten, Issue #2166): Als verbleibende Verwaltung einer Organisation zählt nur noch ein Konto, das handeln kann. Hat eine zweite Person mit Verwaltungsrecht die Löschung ihres Kontos beantragt oder ist ihr Konto deaktiviert, kannst du als letzte aktive Verwaltung weder austreten noch dir das Recht entziehen lassen. Dasselbe gilt, wenn die zweite Verwaltung ein Dienstkonto ist: Es kann keine Mitglieder verwalten und zählt deshalb nicht. Übernimmt beim Kontolöschantrag die dienstälteste Leitung, kommt dafür nur eine Person in Frage, kein Dienstkonto; gibt es keine, verwaist die Organisation (REQ-024 v1.32 AK-10, AK-65).
  • Behoben (API): Kalender-Apps können iCal-Feeds wieder abonnieren (Issue #2226, REQ-015). Der öffentliche Abruf-Endpunkt GET /api/v1/calendar/feeds/{key}/feed.ics?token=…, auf den die beim Anlegen oder Erneuern eines Feeds gezeigte Abo-URL zeigt, war seit März 2026 nicht mehr eingebunden und antwortete 404 — kein Kalender-Abo konnte Ereignisse laden. Er ist wieder da, ohne Anmeldung (der Token in der URL ist die Berechtigung) und in beiden Betriebsmodi. Er liefert text/calendar mit Cache-Control: no-store und nur Ereignisse des Gartens, zu dem der Feed gehört. Fehlender, unbekannter oder rotierter Token, der Token eines anderen Feeds und ein deaktivierter Feed antworten alle gleich mit 404; ein Feed nach seinem Ablaufdatum mit 410. Je Client-IP gilt ein Limit von 30 Abrufen pro Minute (RATE_LIMIT_CALENDAR_FEED), darüber 429. Token und Feed-Schlüssel erscheinen in keiner Zeile des Zugriffs- oder Anwendungslogs. Die Abo-URL baut die API jetzt aus der Vorlage des Endpunkts selbst, sodass beide nicht mehr auseinanderlaufen können. Der Hinweis „Noch nicht implementiert“ im Benutzerhandbuch ist entfernt.
  • Sicherheit (API): iCal-Feeds enden mit der Mitgliedschaft, und die Antwort ist gehärtet (Issue #2226, Security-Review, REQ-015). Der Abruf feed.ics liefert nur noch, solange der Eigentümer des Feeds aktives Mitglied des Gartens mit aktivem Konto ist und der Garten aktiv ist — sonst dieselbe 404; ein Feed ohne Eigentümer liefert nie. Wird ein Mitglied entfernt oder verlässt es den Garten, löscht Kamerplanter seine Feeds dort. Die Leitung listet alle Feeds des Gartens mit GET …/calendar/feeds?scope=all (für andere Rollen 403). Auch einen einzelnen Feed (GET …/calendar/feeds/{key}) siehst du nur, wenn er dir gehört oder du die Leitung bist — für jedes andere Mitglied antwortet ein fremder Feed mit 404 wie ein unbekannter (bisher konnte jedes Mitglied mit dem Schlüssel Name, Eigentümer und Filter eines fremden Feeds lesen). Titel und Beschreibungen werden nach RFC 5545 escaped: CR, LF, CRLF und die Unicode-Umbrüche NEL, LINE SEPARATOR und PARAGRAPH SEPARATOR werden zu einem escapten Zeilenumbruch, übrige Steuerzeichen entfernt, Zeilen auf 75 Oktette gefaltet — ein Aufgabentitel kann keine eigene Eigenschaft (etwa ATTACH) mehr in den Kalender jedes Abonnenten schreiben. Der Standortfilter filters.site_key der Feeds ist entfernt: Er wurde gespeichert, aber nie angewendet, ein „nur Standort X“-Feed lieferte den ganzen Garten; gesendete Werte werden ignoriert, gespeicherte beim Lesen übergangen und beim nächsten Ändern des Feeds entfernt. Die Abo-URL baut die API aus APP_BASE_URL statt aus dem Host-Header der Anfrage, mit https:// außer für localhost. Fehlt APP_BASE_URL das Schema http:///https:// oder der Host, startet das Backend trotzdem, protokolliert aber die Warnung app_base_url_not_absolute_http (ohne den Wert); die Abo-URL ergänzt dann selbst https:// (für localhost, 127.0.0.1 und ::1 http://). Betreiberhinweis: Setze APP_BASE_URL in Produktion auf die vollständige öffentliche https://-Adresse — QR-Codes und die OAuth-Rückruf-URL ergänzen kein Schema. Eine Antwort enthält höchstens 2000 Ereignisse (Fenster 30 Tage zurück bis 90 Tage voraus), und neben dem Limit je IP gilt eines je Token (RATE_LIMIT_CALENDAR_FEED_PER_TOKEN, Standard 60/hour). Betreiberhinweis: Ein Ingress mit aktivem Access-Log protokolliert die Abo-URL samt Token — siehe Benutzerhandbuch Kalender.
  • Sicherheit / Upgrade-Hinweis (API): Die Token der iCal-Feeds liegen nicht mehr im Klartext in der Datenbank (Issue #2171, REQ-015). Bisher stand der Token jedes Kalender-Feeds unverschlüsselt in calendar_feeds.token, und jede Abfrage eines Feeds (GET /api/v1/t/{slug}/calendar/feeds, GET …/feeds/{key}, PUT …/feeds/{key}) gab ihn samt Abo-URL zurück — wer die Datenbank oder eine Sicherung lesen konnte, konnte jeden Feed abrufen. Jetzt speichert Kamerplanter nur noch den SHA-256-Hash (token_hash). Token und ical_url stehen nur noch in der Antwort von POST …/calendar/feeds und POST …/feeds/{key}/regenerate-token, also genau einmal; alle anderen Antworten enthalten weder token noch ical_url. Eine verlorene URL ersetzt du durch eine Token-Erneuerung. Migration v0090 hasht beim ersten Start die vorhandenen Token und entfernt den Klartext: Bereits verteilte URLs lösen weiter ihren Feed auf. Gemessen dabei: Der öffentliche Abruf-Endpunkt GET /api/v1/calendar/feeds/{key}/feed.ics, auf den die Abo-URL zeigt, war seit März 2026 nicht eingebunden (404); Issue #2226 bindet ihn wieder ein (eigener Eintrag). Kein Image-Rollback unter v0090: Ein älteres Image legt den alten eindeutigen Index auf token wieder an und startet nicht, sobald zwei Feeds keinen Klartext-Token mehr haben. API-Clients, die token oder ical_url aus einer Lese-Antwort genutzt haben, müssen auf die Antwort beim Anlegen oder Erneuern umstellen. PUT …/feeds/{key} und POST …/feeds/{key}/regenerate-token stehen jetzt nur dem Mitglied zu, dem der Feed gehört, und der Leitung; für jedes andere Mitglied antworten sie 404 (bisher konnte jedes Mitglied ab Gärtner den Feed eines anderen umbenennen oder dessen URL erneuern). Ein PUT schreibt den Token-Hash nicht mehr zurück, sodass er eine gleichzeitige Rotation nicht rückgängig machen kann. Rolling Update: Ein Feed, den ein noch laufender Pod des alten Images anlegt oder rotiert, nachdem v0090 gelaufen ist, behält seinen Klartext-Token — die Migration läuft nur einmal. Skaliere das alte Deployment vor dem Update herunter, oder prüfe danach mit FOR f IN calendar_feeds FILTER HAS(f, "token") COLLECT WITH COUNT INTO n RETURN n und lass die betroffenen Feeds rotieren.
  • Sicherheit (Mandantentrennung): Die selbst gehostete Pflanzenerkennung nennt dir keine Art mehr, die einem anderen Garten gehört (Issue #2173). Bisher konnte ein Treffer aus dem gemeinsamen Referenzbild-Index den Schlüssel und den wissenschaftlichen Namen einer privaten Art eines anderen Gartens enthalten. Jetzt bekommst du nur Treffer zu globalen Arten, deinen eigenen und solchen, die mit deinem Garten geteilt sind — bei der Bestimmung und bei der Fotoqualitäts-Bewertung. Das gilt auch für Einträge, die schon im Index stehen. Die Referenzbild-Beschaffung indiziert nur noch globale Arten; für die Art eines Gartens antwortet POST /api/v1/admin/reference-images/acquire/{species_key} mit 404. Der Inferenz-Service lehnt einen Nutzerbeitrag (source=user_contributed) ohne is_active=false oder ohne contributed_by/tenant_key mit 422 ab.
  • Behoben (API): Bearbeiten setzt keine Felder mehr zurück, die das Formular nicht enthält. Ein bearbeiteter System-Standorttyp (z. B. „Gewächshaus“) verlor sein System-Kennzeichen und ließ sich danach löschen; jetzt bleibt er ein Systemtyp, und das Löschen antwortet weiter mit 422. Eine bearbeitete Substratmischung bleibt eine Mischung mit ihren Bestandteilen, statt zu einem einfachen Substrat zu werden. Bearbeitest du eine Art, bleiben ihre Eigenschaften (traits), die Schnittmonate und die Eignung als Gründüngung erhalten; eine bearbeitete Wachstumsphase bleibt wiederkehrend und behält die Herkunftsangabe ihres Kc-Werts. Diese Felder pflegt der Server — ein Wert im Anfragekörper wird weiterhin ignoriert.
  • Geändert (Berechtigung): Welche Wetterquellen ein Standort in welcher Reihenfolge nutzt, legt jetzt nur noch die Leitung fest — so, wie es das Rollenmodell vorsieht. Als Gärtner siehst du die Auswahl weiter, kannst sie aber nicht mehr ändern und keine Quelle testen (PUT /api/v1/t/{slug}/sites/{key}/weather-source und POST …/weather-sources/test antworten 403). In der Oberfläche ist der Abschnitt „Wetterquelle“ für dich dann schreibgeschützt (Issue #2181).
  • Behoben: Das Bearbeiten eines Standorts, eines Bereichs oder eines Stellplatzes setzte Werte zurück, die der Server selbst pflegt. Ein bearbeiteter Standort verlor die Liste seiner aktiven Wetterquellen (weather_source_priority). Ein bearbeiteter Bereich hatte danach Tiefe 0 und keinen Pfad und erschien im Standortbaum wie ein oberster Bereich. Ein bearbeiteter, belegter Stellplatz galt danach als frei und konnte bei der nächsten Belegung erneut vergeben werden. Diese Werte bleiben jetzt erhalten; Tiefe und Pfad eines Bereichs werden wie beim Anlegen aus dem übergeordneten Bereich abgeleitet (Issue #2181).
  • Behoben (Einladungen), Issue #2162: Eine E-Mail-Einladung in einen Garten wurde nie verschickt — die Einstellungsseite meldete „Einladung gesendet“ und verwarf den Link. Jetzt geht nach dem Anlegen eine E-Mail mit dem Annahme-Link ({FRONTEND_URL}/invitations/accept?token=…, 7 Tage gültig) an die eingeladene Adresse; sie nennt weder Garten noch Einladenden. Geändert (API): POST /api/v1/tenants/{slug}/invitations/email und …/invitations/link liefern zusätzlich accept_url, die E-Mail-Einladung außerdem delivered. Bei delivered: false (kein zustellender E-Mail-Adapter — auch EMAIL_ADAPTER=console ohne DEBUG —, Versand abgewiesen oder Transportfehler) besteht die Einladung trotzdem; die Seite sagt das und zeigt den Link zum Weitergeben. Ein Einladungslink kopiert jetzt den Link statt des nackten Tokens. Wer einen Einladungslink ohne Sitzung öffnet, landet nach der Anmeldung — auch über einen Anmeldeanbieter — wieder auf der Annahme-Seite; der Link zur Registrierung übernimmt die Einladung. Begrenzt: POST …/invitations/email erlaubt je Konto RATE_LIMIT_INVITATION_EMAIL (Standard 10/minute) und höchstens TENANT_INVITATION_EMAILS_PER_DAY E-Mail-Einladungen in 24 Stunden (Standard 50, über alle Mandanten); darüber 429 RATE_LIMIT_EXCEEDED, nichts wird angelegt oder verschickt. Eine Einladung, die nicht mehr angenommen werden kann (#2180), öffnet auch die Registrierung bei invite_only oder Domain-Liste nicht mehr.
  • Sicherheit (Einladungen), Issue #2180: Das Annehmen einer Einladung prüft die Rangregel erneut (REQ-024 AK-58): Eine Leitungs-Einladung in den Mandanten platform, deren Absender dort nicht (mehr) Leitung ist, antwortet mit 403 und legt nichts an. Vorher machte eine solche Einladung — ausgestellt vor der Regel (#2084) von einem Mitglied mit Verwaltung, aber ohne Leitung — den Annehmenden zum Plattform-Admin. Migration v0089 widerruft beim Update jede offene Leitungs-Einladung in platform und protokolliert nur ihre Anzahl (revoke_platform_lead_invitations_applied). Wer eine solche Einladung offen hatte, muss neu eingeladen werden — von einer Plattform-Leitung. Bereits angenommene Einladungen dieser Art ändert die Migration nicht — wie du solche Plattform-Leitungen findest und sichtest, steht unter Plattform-Admin-Bereich.
  • Sicherheit (Mandantentrennung): Der Aktivitäten-Katalog (GET /api/v1/activities, GET /api/v1/activities/{key}) zeigt dir die globalen Aktivitäten und die deines aktiven Gartens, nie die eines anderen Gartens; ein fremder Schlüssel antwortet „nicht gefunden“. Ein automatisch erzeugter Aktivitätsplan, den alle Gärten teilen, entsteht nur noch aus globalen Aktivitäten (Issue #2119). Heute legen nur Plattform-Admins Aktivitäten an, und alle sind global — für dich ändert sich nichts.
  • BREAKING (API): Wissensfrage nur noch im Garten und hinter der KI-Freigabe (Issue #2175, REQ-031 v2.5, Szenario 15). POST /api/v1/knowledge/ask ist entfernt (kein Alias, 404). Die Frage an die Wissensbasis heißt jetzt POST /api/v1/t/{slug}/ai/knowledge/ask — gleicher Response-Body; im Request gilt jetzt top_k ≤ 10 (vorher 20) und der Pflanzenkontext ist begrenzt (species, phase, substrate je höchstens 100 Zeichen, ec 0–20, ph 0–14, sonst 422). Die alte Route beantwortete jedem angemeldeten Konto, auch Betrachtern und Service Accounts, eine Frage mit einem Sprachmodell-Aufruf: ohne KI-Schalter, ohne Einwilligung, ohne Tagesbudget. Die neue Route prüft wie „Warum?“ und Chat ab Rolle Gärtner (Betrachter 403), den Betreiber-Schalter AI_FEATURES_ENABLED (404), die KI-Einstellung des Gartens (403 ai.disabled_for_tenant), die eigene Einwilligung ai_knowledge_question (403 CONSENT_REQUIRED; mit Pflanzenwerten im context zusätzlich ai_tenant_data_access; antwortet der Knowledge Service mit einem Cloud-Modell der Plattform, zusätzlich ai_cloud_processing und die Cloud-Freigabe des Gartens, sonst 403 ai.disabled_for_tenant — maßgeblich ist der System-Standard-Provider, nicht der Provider des Gartens) und das KI-Tagesbudget (429 AI_BUDGET_EXCEEDED) — alles, bevor der Knowledge Service angefragt wird. Das Minutenbudget RATE_LIMIT_INFERENCE je Konto bleibt. Ist der Knowledge Service nicht erreichbar, antwortet sie 502. Im Light-Modus gibt es keine Einwilligung; dort ist die Wissensfrage POST /api/v1/public/ai/ask. GET /api/v1/knowledge/search (ohne Sprachmodell) bleibt unverändert.
  • BREAKING (API): /api/v1/public/ai/* nur noch im Light-Modus (REQ-031 v2.6 §5.3, REQ-027 v1.8). POST /api/v1/public/ai/ask und GET /api/v1/public/ai/health waren in beiden Modi ohne Anmeldung erreichbar — im Full-Modus beantwortete die Frage-Route jedem anonymen Aufrufer eine Frage mit einem Sprachmodell-Aufruf, an Einwilligung, KI-Schalter des Gartens und Tagesbudget vorbei. Im Full-Modus existieren beide Pfade nicht mehr (404); die Wissensfrage ist dort POST /api/v1/t/{slug}/ai/knowledge/ask. Im Light-Modus verlangen sie jetzt einen angemeldeten Principal, den der Light-Modus ohne Login als System-User liefert — für die Light-Oberfläche ändert sich nichts. Das Adress-Limit AI_PUBLIC_RATE_LIMIT_PER_MIN und der Betreiber-Schalter AI_FEATURES_ENABLED gelten weiter.
  • Datenschutz (Referenzbild-Beitrag, Issue #2174, REQ-034 v1.4 AC-17): Ein Identifikationsfoto als Referenz für die selbst-gehostete Erkennung beizusteuern (POST /api/v1/t/{slug}/identification/reference) setzt jetzt die Einwilligung „Foto-Beitrag zur Pflanzenerkennung“ (reference_contribution) voraus — wie schon der automatische Beitrag aus der Galerie. Ohne erteilte oder nach widerrufener Einwilligung antwortet die API mit 403 (CONSENT_REQUIRED); dabei werden weder dein Tages-Kontingent verbraucht noch ein Beitrag vermerkt oder das Bild verarbeitet. Die Art wird nur noch unter deinem Mandanten aufgelöst: Eine private Art eines anderen Mandanten antwortet wie eine unbekannte mit 404. Verhaltensänderung: Im Light-Modus ist der Beitrag nicht mehr möglich und antwortet mit 409 (ADAPTER_NOT_AVAILABLE) — dort gibt es keine Einwilligungsverwaltung, und REQ-034 schaltet den Referenz-Beitrag im Light-Modus ab.
  • Datenschutz (Schädlingserkennung, REQ-044 v1.3/v1.4, REQ-010 v1.6/v1.7): Ein von einer Plattform-Admin global freigegebenes Schädlingsfoto fließt nur noch dann als Referenz in den Erkennungs-Index ein, wenn die Person, die es beigetragen hat, die eigene Einwilligung „Foto-Beitrag zur Schädlingserkennung“ (pest_reference_contribution, REQ-044 v1.4, REQ-010 v1.7) erteilt hat. Die Einwilligung „Foto-Beitrag zur Pflanzenerkennung“ (reference_contribution) nennt nur Galerie-Fotos von Pflanzen und öffnet den Schädlingsindex nicht; bestehende Einwilligungen dafür werden nicht übernommen — wer beiträgt, muss neu einwilligen. Ohne erteilte oder nach widerrufener Einwilligung — und immer im Light-Modus — bleibt das Foto freigegeben und sichtbar, der Hintergrund-Task endet aber ohne Aufruf des Inferenz-Dienstes (no_consent bzw. light_mode). Ein Widerruf entfernt bereits indexierte Vektoren nicht; das tut das Löschen des Fotos oder des Kontos.
  • Geändert (API): Glossar-Erklärung erzeugen nur mit KI-Freischaltung des Gartens (REQ-035 v1.4). POST /api/v1/t/{slug}/glossary/term/{slug}/generate verlangt zusätzlich zur Rolle die Garten-Freischaltung (REQ-031 Stufe 2): ausgeschaltet → 403 (AI_DISABLED_FOR_TENANT), bevor Zwischenspeicher, Tageskontingent oder Sprachmodell berührt werden. Verhaltensänderung: Bei instanzweit abgeschalteter KI antwortet die Route jetzt mit 404 statt mit der kuratierten Kurzdefinition. Lesen (GET …/glossary/terms, GET …/glossary/term/{slug}, öffentliche und Admin-Routen) ist unverändert.
  • Knowledge Service, Sicherheit (Issue #2176): Ein unbekannter Wert in LLM_PROVIDER fiel bisher still auf den Anthropic-Adapter zurück — ein Tippfehler wie openai-compatible schickte jede Frage samt des für den lokalen Server gedachten LLM_API_KEY an die Cloud. Jetzt startet der Knowledge Service mit einem unbekannten Wert nicht und nennt die erlaubten Werte (anthropic, ollama, openai_compatible).
  • Sicherheit (Mandantentrennung, Hygiene-Paket, Issue #2144): Eine Reihe kleiner Lücken aus dem Mandanten-Audit ist geschlossen. Jede Antwort unter /api/v1/ trägt jetzt Cache-Control: no-store, wenn die Route nichts anderes festlegt — Anhänge, Schädlingsbilder und der Chat-Stream behalten ihre eigene Angabe. „Substrat-Charge für Wiederverwendung vorbereiten" verlangt mindestens die Rolle Gärtner (Beobachter: 403). Das Ändern der Zusatzberechtigungen eines Mitglieds verlangt dieselbe Bestätigung wie ein Rollenwechsel. Als Plattform-Admin kannst du dein eigenes Konto und das letzte Konto mit Plattform-Rolle nicht mehr deaktivieren (422). Ein Kommentar-Schlüssel, der nicht zur angegebenen Aufgabe gehört, antwortet 404 statt 422; der CSV-Import sieht beim Abgleich nur die Arten, die du auch in der Artenliste siehst. Eine Benachrichtigung ohne Empfänger wird nicht mehr gespeichert und ist für niemanden abrufbar. Mischkultur-Listen, -Zähler und -Empfehlungen zeigen nur Arten, die du sehen darfst; Mischkultur-Kanten verbinden nur globale Arten. Filter-Feldnamen in Datenbankabfragen werden geprüft, bevor eine Abfrage entsteht.
  • Geändert (MCP): Ein idempotency_key, der mit anderen Argumenten erneut gesendet wird, antwortet mit conflict.idempotency_key_reused (409 am REST-Alias) statt das Ergebnis des ersten Aufrufs zu wiederholen; ein abgelaufener Eintrag wird nicht mehr wiederholt (Issue #2144).
  • Neu (Betrieb): python -m app.migrations.audit_legacy_stamps listet nur lesend Zeilen, deren Autor kein Mitglied ihres Gartens ist — Spuren der frühen Mandanten-Nachrüstung (v0004). Migration v0088 entfernt diesen Stempel aus Schädlingen, Krankheiten und Behandlungen. Details: src/backend/app/migrations/README.md (Issue #2144).
  • Neu (API): Service Accounts für einen Garten (Issue #2137, interne Referenz MT-041). Wer in einem Garten Leitung ist und die Zusatzberechtigung Technik hat, legt mit POST /api/v1/t/{slug}/service-accounts ein Konto für eine Maschine an (Home Assistant, Grafana, CI/CD) — ohne Passwort, als viewer oder grower, mit einem ersten API-Key, der nur in diesem Garten gilt. Dazu Liste, Key-Rotation (…/{sa_key}/rotate-key, alte Keys sofort widerrufen oder nach einem Übergangsfenster von bis zu 24 h) und Entfernen (DELETE …/{sa_key}). Jede Änderung verlangt dein Passwort (Step-up service_account_change) und steht im Sicherheitsprotokoll. Höchstens TENANT_MAX_SERVICE_ACCOUNTS (Default 20) je Garten; ein Service Account belegt einen Platz im Mitgliederlimit. Light-Modus: nicht verfügbar. Siehe Service Accounts & API-Keys
  • Neu (API): Ein API-Key bekommt beim Ausstellen eine IP-Allowlist (ip_allowlist, CIDR, nicht weiter als /8 bzw. /32), ein Ratenlimit (rate_limit_per_minute, 1–10000) und ein Ablaufdatum (expires_at, höchstens 730 Tage) — für POST /api/v1/auth/api-keys wie für Service Accounts. Durchgesetzt wurden die drei Werte schon, setzen ließen sie sich bisher nicht. Ein unbrauchbarer Wert ist 422, bevor nach dem Passwort gefragt wird; die Key-Liste zeigt die Werte (Issue #2137)
  • Geändert (API): Ein Service Account kann keinen Garten gründen (POST /api/v1/tenants) und keine Einladung annehmen (403); die Mitgliederverwaltung macht ihn nicht zur Leitung (403). Service Accounts verwaltet jetzt Leitung und Technik statt der Zusatzberechtigung Verwaltung (Issue #2137)
  • Sicherheit (Mandantentrennung): Schreibende Operationen auf Standorte, Bereiche, Stellplätze und Nährstoffpläne prüfen die Zugehörigkeit zum Mandanten jetzt im Dienst selbst und nicht mehr nur im aufrufenden Endpunkt (Issue #2107). Für dich ändert sich nichts; ein fremder Schlüssel antwortet weiter mit „nicht gefunden“. Das Entfernen eines Nährstoffplans von einer Pflanze und das Ändern oder Löschen einer Aktivität im gemeinsamen Katalog werden ebenfalls im Dienst geprüft (Issue #2120). Migration v0087 entfernt das nie befüllte Feld tenant_key aus gespeicherten Bereichen und Stellplätzen. Dasselbe gilt jetzt für Pflanzdurchläufe (Anlegen, Ändern, Löschen, Einträge, Pflanzen anlegen, übernehmen und lösen, Phasenwechsel, Phasendaten, Sammelentfernen, Nährstoffplan entfernen) und Tanks (Löschen, Zustand, Wartung, Wartungsplan, Befüllung). Ebenso für Düngeereignisse, Gießprotokolle und Gießereignisse (Ändern, Löschen, Anlegen), Erntechargen (Ändern, Qualität, Ertrag, Ernte abschließen), Sukzessionspläne (Ändern, Löschen, Durchläufe erzeugen), Nachernte-Chargen, Überwinterungsprofile, Aktivitätspläne und das Bestätigen eines CSV-Imports. Schließt du die Ernte eines ganzen Durchlaufs ab, beendet das nur noch deine eigenen Pflanzen, auch wenn ein Altdatensatz den Durchlauf mit einer fremden Pflanze verknüpft. Dasselbe gilt für das Anwenden eines Aktivitätsplans auf einen Durchlauf, das Sammelentfernen, die Phasendaten und das Umsetzen der Pflanzen eines Durchlaufs, Tagebucheinträge im Durchlauf und das Bestätigen einer Bewässerung: Sie berücksichtigen nur deine eigenen Pflanzen. Beim Bestätigen einer Bewässerung wird der Nährstoffplan eines Durchlaufs nur verwendet, wenn du ihn lesen darfst. Das Umsetzen eines Durchlaufs mit Pflanzen auf Stellplätzen an einen anderen Bereich bricht nicht mehr mit einem Serverfehler ab. Die Dienste für Aktoren, Aquaponik-Systeme und InvenTree (Verbindungen, Verknüpfungen, Ausrüstung) nehmen den Mandanten jetzt in jeder Methode nur noch als benanntes Argument entgegen, sodass ein vertauschter Schlüssel nicht mehr unbemerkt bleibt; für dich ändert sich nichts. Dasselbe gilt jetzt für die Fotogalerie einer Pflanze (Foto hochladen und verknüpfen, Titelbild, Bildunterschrift und Aufnahmedatum, Qualitätsprüfung, Foto löschen). Ebenso für Arten, Sorten, Substrate, Substratmischungen und Substratchargen (Anlegen, Ändern, Löschen): Der Dienst verlangt deinen Mandanten, deine Rolle und die Plattform-Admin-Angabe jetzt immer ausdrücklich, statt ohne sie ungeprüft zu schreiben. Das Ändern und Löschen von Einträgen im Phasenverlauf einer Pflanze prüft den Mandanten jetzt im Dienst selbst. Ebenso für Pflanzen (Anlegen, Ändern, Entfernen), Behandlungsanwendungen im Pflanzenschutz, Phasen eines Workflows (Anlegen, Ändern, Löschen) und das Duplizieren eines Workflows, Vermehrungschargen (Anlegen, Abschließen), das Löschen eines Tagebucheintrags, die Winterhärtezone eines Standorts und die nächtliche Prüfung des Winterquartier-Klimas. Eine Workflow-Phase ohne zugehörigen Workflow lässt sich weiterhin nicht ändern oder löschen; die Antwort bleibt „nicht gefunden“.
  • BREAKING (API): KI-Aufrufe haben ein Tagesbudget pro Konto und pro Garten (Issue #2110). Jede Route, die ein Sprachmodell anspricht — Tipps und Tagestipp erzeugen, „Warum?“, Chat-Nachricht, Glossar-Erklärung erzeugen, KI-Diagnose —, zählt gegen AI_BUDGET_USER_CALLS_PER_DAY (Standard 50 je Konto und Garten), AI_BUDGET_TENANT_CALLS_PER_DAY (500 je Garten) und AI_BUDGET_TENANT_TOKENS_PER_DAY (2000000 Tokens je Garten), jeweils pro UTC-Tag. Darüber antwortet die API mit 429 und AI_BUDGET_EXCEEDED (details[0].code nennt das Budget) sowie Retry-After bis Tagesende. Ist Valkey nicht erreichbar, läuft kein KI-Aufruf (503 AI_BUDGET_UNAVAILABLE). Zusätzlich gilt auf diesen Routen das Minutenbudget RATE_LIMIT_INFERENCE je Konto. Das KI-Aufruf-Protokoll speichert je Aufruf die Token-Zahlen (prompt_tokens, completion_tokens), damit die Kosten eines Gartens zählbar sind. Eine abgelehnte Chat-Nachricht kommt jetzt als HTTP-Fehler statt als abgebrochener Stream; die Oberfläche erklärt jede Ablehnung.
  • Behoben (API): Eine 429-Antwort aus einer Grenze des Fachcodes (Tageslimits von Pflanzenbestimmung und Referenzbeitrag, API-Key-Limit, KI-Budget) trägt jetzt den Header Retry-After (Issue #2110). Mit #2109 bekam ihn nur die Minutenbegrenzung; die Wartezeit der übrigen stand bisher nur im Meldungstext.
  • Glossar: Ob eine erzeugte Glossar-Erklärung als Cloud-Verarbeitung gilt, entscheidet jetzt der Standard-Provider der Plattform, nicht der Standard des Gartens, der den Begriff zuerst angefragt hat (Issue #2110). Die Erklärung wird allen Gärten ausgeliefert; ihr Cloud-Hinweis ist jetzt für alle gleich und steht am gespeicherten Eintrag.
  • Verhaltensänderung (API): Sechs Listen, die bisher jeden Eintrag lieferten, lesen jetzt ein begrenztes Fenster mit offset (Standard 0) und limit (Standard 50, höchstens 200; Issue #2131, MT-035): GET /admin/platform/tenants, GET /admin/platform/users, GET /tenants/{slug}/invitations, GET /t/{slug}/ai/conversations, GET /t/{slug}/tasks/plants/{plant_key} und GET /t/{slug}/post-harvest/{key}/observations. Die Antwort bleibt ein Array; ein Client ohne limit erhält jetzt die ersten 50 Einträge statt aller und muss weitere Seiten mit offset lesen. Mandanten, Nutzer, Einladungen und Unterhaltungen sind neueste zuerst sortiert, Aufgaben nach Fälligkeit, Beobachtungen neueste zuerst — bei gleichem Zeitstempel entscheidet der Schlüssel, damit keine Seite einen Eintrag doppelt liefert oder auslässt.
  • API: GET /t/{slug}/plant-instances, /watering-logs, /watering-events und /feeding-events nehmen zusätzlich after=<key> an und blättern damit per Schlüssel statt per offset (Issue #2131). Diese vier Listen zählen außerdem nicht mehr bei jeder Seite alle Einträge des Gartens nach — die Zahl stand nie in der Antwort. Ein neuer Index (tenant_key, _key) auf den vier Collections entsteht beim nächsten Start; gemessen auf ArangoDB 3.12 mit 50 000 Einträgen eines Gartens sinkt eine Seite von 167 ms (Seite 1) bzw. 262 ms (letzte Seite per Cursor) auf etwa 4 ms bei den Pflanzen und die erste Seite eines kleinen Gartens von bis zu 132 ms auf etwa 4 ms bei Gieß- und Düngeereignissen.
  • API: POST /calculations/sun-times-range berechnet höchstens 366 Tage pro Anfrage; ein längerer Zeitraum wird mit 422 abgelehnt (Issue #2131). Bisher rechnete ein Aufruf jeden Tag zwischen beliebigen Daten.
  • Verhaltensänderung (API): Sechs weitere Listen lesen jetzt ein begrenztes Fenster mit offset (Standard 0) und limit (Standard 50, höchstens 200; Issue #2131, MT-035): GET /tenants/{slug}/members, GET /tenants/{slug}/assignments, GET /admin/platform/tenants/{tenant_key}/members, GET /t/{slug}/tasks/workflows/{key}/executions, GET /t/{slug}/inventree/references und GET /t/{slug}/propagation/stats/by-cultivar. Die Antwort bleibt ein Array; ein Client ohne limit erhält die ersten 50 Einträge statt aller. Mitglieder und Zuweisungen sind älteste zuerst sortiert, Durchläufe neueste zuerst, Verknüpfungen nach Collection, die Statistik nach Sorten-Schlüssel — bei Gleichstand entscheidet der Schlüssel. Zwei neue Indizes entstehen beim nächsten Start: (tenant_key, created_at) auf location_assignments und workflow_template_key auf workflow_executions; gemessen auf ArangoDB 3.12 mit 20 000 Einträgen las die erste Seite vorher die ganze Collection, jetzt nur die Einträge des Gartens bzw. der Vorlage.
  • API: Dreizehn Listen eines Gartens zählen nicht mehr bei jeder Seite alle passenden Einträge nach — die Zahl stand nie in der Antwort (Issue #2131, MT-035): GET /t/{slug}/tasks, GET /t/{slug}/planting-runs, GET /t/{slug}/planting-runs/{key}/diary, GET /t/{slug}/planting-runs/{key}/plants/{plant_key}/diary, GET /t/{slug}/plant-instances/{key}/diary, GET /t/{slug}/ipm/plants/{plant_key}/inspections, GET /t/{slug}/ipm/plants/{plant_key}/treatment-applications, GET /t/{slug}/harvest/plants/{plant_key}/observations, GET /t/{slug}/harvest/batches, GET /t/{slug}/post-harvest, GET /t/{slug}/propagation/events, GET /t/{slug}/propagation/batches und GET /import/jobs. Antwort, Fenster und Filter bleiben gleich; bei gleichem Zeitstempel entscheidet jetzt der Schlüssel über die Reihenfolge, damit keine Seite einen Eintrag doppelt liefert oder auslässt.
  • API: Fünfzehn weitere Listen zählen nicht mehr bei jeder Seite alle passenden Einträge nach — die Zahl stand nie in der Antwort (Issue #2131, MT-035): GET /activities, GET /ipm/pests, GET /ipm/diseases, GET /ipm/treatments, GET /phase-definitions, GET /phase-sequences, GET /t/{slug}/sites, GET /t/{slug}/tanks, GET /t/{slug}/fertilizers, GET /t/{slug}/nutrient-plans, GET /t/{slug}/succession-plans, GET /t/{slug}/overwintering-profiles, GET /t/{slug}/propagation/protocols, GET /t/{slug}/tasks/workflows und GET /t/{slug}/harvest/indicators. Dasselbe gilt für das MCP-Werkzeug get_harvest_readiness, die Inspektions-Abfrage der Diagnose-Werkzeuge und den Inspektionsplan einer Pflanze. Antwort, Fenster und Filter bleiben gleich; bei gleichem Namen entscheidet bei Nährstoffplänen, Bewurzelungsprotokollen, Phasen-Definitionen und Workflow-Vorlagen jetzt der Schlüssel über die Reihenfolge, damit keine Seite einen Eintrag doppelt liefert oder auslässt.
  • Behoben (Datenschutz): Sechs Aufräum-Tasks liefen seit ihrer Einführung (Juli 2026) bei jedem Aufruf mit TypeError auf und löschten nichts (Issue #2150, NFR-011): ai.cleanup_expired_conversations, ai.cleanup_expired_audit_log, glossary.cleanup_expired_cache, glossary.invalidate_after_reingest, mcp.cleanup_expired_audit_log, mcp.cleanup_expired_idempotency. Sie öffnen ihre Datenbankverbindung jetzt wie alle anderen Tasks über get_db(). Nach dem Update holen die ersten Läufe den Rückstand nach: KI-Audit-Einträge älter als 30 Tage, abgelaufene Glossar-Cache-Zeilen und MCP-Idempotenz-Datensätze älter als 24 Stunden werden gelöscht. Weil der Idempotenz-Datensatz bisher nie verschwand, lieferte ein MCP-Schreibwerkzeug mit wiederverwendetem idempotency_key auch nach mehr als 24 Stunden das gespeicherte Ergebnis statt neu auszuführen; jetzt gilt wieder das Fenster aus REQ-033 von 24 Stunden — ein abgelaufener Datensatz wird nicht mehr wiederholt, auch bevor der stündliche Lauf ihn löscht (Issues #2144, #2169). KI-Unterhaltungen (90 Tage) und das MCP-Audit (90 Tage) hatten noch keine fälligen Einträge.
  • Datenschutz: Der persönliche Datenexport (Art. 15/20) enthält jetzt deinen persönlichen Garten (Issue #2135): Standorte mit GPS-Koordinaten, Beete, Stellplätze, Pflanzen, Pflanzdurchläufe, alle Aufgaben, das Tagebuch und Anhang-Metadaten — begrenzt auf die persönlichen Gärten, die dir gehören, ohne Kontokennungen anderer Mitglieder. Aus Organisationen bleiben wie bisher nur die Mitgliedschaft und deine eigenen Beiträge im Export. Sensor-Messwerte sowie Tank-, Gieß- und Düngeprotokolle sind noch nicht enthalten.
  • Datenschutz: Eine Kontolöschung lässt keine Organisation mehr ohne Verwaltung zurück (Issue #2134). War die Person die letzte mit Verwaltungsrecht, übernimmt die dienstälteste Leitung die Verwaltung (Sicherheits-Audit via: account_erasure, die Mitglieder werden benachrichtigt). Bleibt keine Leitung oder war sie das einzige Mitglied, wird die Organisation orphaned und nach der Frist der Mandantenlöschung gelöscht; Mitglieder und Platform-Admins werden benachrichtigt. Bisher blieb eine solche Organisation unbefristet und unverwaltbar bestehen. Die Löschvorschau (GET /privacy/erasure-preview, GET /admin/platform/users/{key}/erasure-preview) nennt beide Fälle im neuen Feld organizations. Die in REQ-023 §5a.5 skizzierte Notfall-Verwaltung entfällt.
  • BREAKING (API): Eine Mandantenlöschung wird jetzt geplant statt sofort ausgeführt (Issue #2123, REQ-024 AK-52). DELETE /tenants/{slug} und DELETE /admin/platform/tenants/{key} antworten weiter mit 202, aber mit status: "scheduled" und scheduled_for: Der Mandant ist bis dahin für alle gesperrt, seine Mitglieder erhalten eine E-Mail mit dem Datum, und erst nach RETENTION_TENANT_ERASURE_GRACE_DAYS Tagen (Standard 90, 0 = sofort wie bisher) löscht der tägliche Lauf. Bis dahin bricht POST /tenants/{slug}/erasure/cancel (Leitung und Verwaltung, Passwort) oder POST /admin/platform/tenants/{key}/erasure/cancel (Platform-Admin) die Löschung ab. Mandanten haben dazu einen Zustand status (active, suspended, pending_deletion, orphaned, deleted) statt des Felds is_active; Migration v0085 stellt bestehende Mandanten um, is_active bleibt in den Antworten als abgeleiteter Wert erhalten. Ein Platform-Admin kann einen Mandanten mit geplanter Löschung nicht mehr über den Aktiv-Schalter reaktivieren (422) — dafür gibt es den Abbruch.
  • Sicherheit (Sitzungen): Refresh-Tokens bilden jetzt je Anmeldung eine Familie (Issue #2116, MT-019, REQ-023 §3.2a). Wird ein bereits rotiertes Refresh-Token erneut vorgelegt, endet die ganze Familie; der Vorfall wird als auth.refresh_replay_detected protokolliert. Innerhalb von 60 Sekunden erhält derselbe Client (gleicher User-Agent) auf dem Cookie-Weg stattdessen ein Access Token ohne neuen Cookie (zwei Tabs erneuern gleichzeitig). Der Body-Weg gekoppelter Geräte hat kein Gnadenfenster. Rotierte Tokens bleiben bis zu ihrem eigenen Ablauf gespeichert, damit auch eine späte Wiederverwendung erkannt wird. Access Tokens enden mit ihrer Sitzung: Sie tragen die Token-Generation des Kontos (Claim gen); jeder Widerruf — Abmelden, alle Sitzungen abmelden, eine Sitzung beenden, Passwortänderung oder -Reset, Deaktivierung, Art.-17-Antrag, E-Mail-Wechsel und dessen Rücknahme, erkannte Wiederverwendung — erhöht sie, sodass ein vorher ausgestelltes Token ab der nächsten Anfrage mit 401 abgelehnt wird, statt seine 15 Minuten auszuschöpfen (REQ-023 AK-14). Das Beenden einer Sitzung beendet deshalb auch die Access Tokens der übrigen Sitzungen des Kontos; diese erneuern sich unbemerkt. Kein zusätzlicher Datenbankzugriff je Anfrage (das Konto wird ohnehin gelesen). Eine Deaktivierung durch den Admin beendet jetzt ebenfalls alle Sitzungen. Vor dem Update ausgestellte Tokens bleiben bis zu ihrem Ablauf gültig; keine Migration, kein erzwungenes Neuanmelden.
  • Sicherheit: Passwort-Reset- und E-Mail-Bestätigungs-Tokens werden nur noch als SHA-256-Hash gespeichert (password_reset_token_hash, email_verification_token_hash; Issue #2158). Vorher war der gespeicherte Wert der Link selbst; wer die Datenbank oder ein Backup lesen konnte, konnte innerhalb der Reset-Stunde ein Konto übernehmen. Migration v0086 hasht die gespeicherten Werte an Ort und Stelle; vor dem Update verschickte Links funktionieren noch genau einmal.
  • Web-Push: Ein Endpunkt gehört immer nur einem Konto. Wird er abonniert, verschwindet er bei allen anderen Konten (Issue #2117) — auf einem gemeinsam genutzten Gerät erhält der vorherige Nutzer darüber keine Pushes mehr.
  • BREAKING (API), Sicherheit: Kamerplanter ist mit einer Home-Assistant-Instanz verbunden (der des Betreibers) und nutzt sie für alle Mandanten — dokumentiert als „Single-Household-Instanz" (Issue #2112, MT-015, REQ-005 §4c, REQ-018, REQ-030). Welche Entitäten dieser Instanz ein Mandant nutzen darf, legt jetzt der Plattform-Admin je Mandant fest (neue Collection tenant_ha_entity_grants, Endpunkte /api/v1/admin/ha-entity-grants/tenants/{tenant_key} für Liste, Inventur, Freigeben und Zurückziehen). Sensor, Aktor, HA-Wetterquelle und HA-Benachrichtigungsziel mit nicht freigegebener Entität → 422 HA_ENTITY_NOT_GRANTED (wertfrei). …/sensors/live, Tank-Live-Status, Frostwarnung, Ingest, Tank-Sync, Aktor-Sync, Regelkreis und Wetterabruf lesen nicht freigegebene Entitäten nicht mehr (im Log nur gezählt); ein nicht freigegebener Aktor fällt auf die manuelle Aufgabe zurück; HA-Benachrichtigungen gehen nur an Ziele, die dem Mandanten der Benachrichtigung freigegeben sind — auch der Standard notify.notify. Breaking: GET /t/{slug}/ha/weather-entities, /ha/sensor-entities und /tanks/ha-entities verlangen jetzt den Scope technical (vorher jedes Mitglied, auch Beobachter) und liefern wie actuators/integrations/home-assistant/entities nur freigegebene Entitäten. Migration v0084 legt die Collection an und gibt jede bereits referenzierte Entität dem referenzierenden Mandanten frei, damit nach dem Update nichts stehen bleibt. Im Light-Modus (ein Haushalt, kein Admin-Bereich) gibt das Eintragen einer Entität sie frei.
  • Neu (Betrieb): Wer ein Konto anlegen darf, ist jetzt einstellbar (Issue #2132, MT-036, REQ-023 §3.2d). REGISTRATION_MODE = open (Default — keine Änderung für bestehende Installationen), invite_only oder closed, dazu optional REGISTRATION_ALLOWED_DOMAINS (kommagetrennte E-Mail-Domains). Es gilt für POST /api/v1/auth/register und für die erste Anmeldung über einen OIDC-Anbieter. Eine offene E-Mail-Einladung für genau diese Adresse ist die Ausnahme von invite_only und von der Domain-Liste — lokal mit ihrem Token (invitation_token im Body), über OIDC mit der vom Anbieter bestätigten Adresse; closed lässt niemanden zu, auch nicht mit Einladung. Eine Ablehnung ist 403 (REGISTRATION_NOT_ALLOWED), für vergebene wie freie Adressen gleich. GET /api/v1/mode meldet registration: {mode, domain_restricted}. Bestehende Konten melden sich in jedem Modus an
  • BREAKING (API): Das Mitgliederlimit eines Mandanten wirkt jetzt (Issue #2133, MT-037, REQ-024 AK-64). Ist ein Mandant voll, antworten die Einladungsannahme (POST /api/v1/tenants/invitations/accept) und das Hinzufügen durch einen Plattform-Admin mit 422 (MEMBER_LIMIT_REACHED); die Einladung bleibt offen. Wirksam ist min(max_members, TENANT_MAX_MEMBERS_CEILING) — die neue Plattform-Obergrenze, Default 50. max_members über der Obergrenze wird beim Anlegen und Ändern mit 422 abgelehnt; eine Organisation ohne Angabe erhält die Obergrenze. Ein persönlicher Mandant hat das Limit 1: Wer jemanden in seinen persönlichen Garten einladen will, hebt das Limit vorher an (PATCH /api/v1/tenants/{slug} mit max_members). Bestehende Mitglieder bleiben in jedem Fall erhalten, auch wenn ein Mandant heute mehr hat als sein Limit — nur der nächste Beitritt wird verweigert. Keine Migration
  • BREAKING (API, Datenschutz): Die Einwilligungszwecke hibp_check und external_enrichment werden nicht mehr angeboten (Issue #2136, MT-040, REQ-025 v1.31 §3.6) — kein Code las sie: Eine HaveIBeenPwned-Prüfung gibt es nicht, und die GBIF/Perenual-Anreicherung sendet nur Artnamen. GET /api/v1/privacy/consents und die Datenschutzerklärung listen sie nicht mehr; POST /api/v1/privacy/consents und DELETE /api/v1/privacy/consents/{purpose} antworten für sie mit 422. Gespeicherte Einwilligungen bleiben unverändert (Export, Löschung, R-04 wie bisher; keine Migration). Der Zweck error_tracking heißt jetzt „Fehlerberichte meinem Konto zuordnen (Sentry)" und wirkt: Ein Fehlerbericht trägt die Konto- und Mandanten-Pseudonyme nur mit dieser Einwilligung. Der Hinweis im Einwilligungs-Banner nennt HaveIBeenPwned und Stammdatenanreicherung nicht mehr.

  • Fehler-Tracking: Ein Ereignis einer authentifizierten Anfrage trägt im Nutzer-Block die Pseudonyme id=sub_… und tenant=ten_… (Issue #2129, NFR-011 v1.37). Was das Sentry-SDK oder eine Integration selbst als Nutzer setzt, wird verworfen — auch in inference- und knowledge-service (gemeinsames Modul kp_errortracking).

  • Betrieb (Logs, Celery, MCP): Logzeilen einer Anfrage tragen jetzt neben request_id die Pseudonyme actor (Konto, sub_…) und tenant (Mandant, ten_…), sobald die Anfrage authentifiziert bzw. ihr Mandant aufgelöst ist (Issue #2130, MT-034, NFR-011 v1.37). Ein aus der Anfrage gestarteter Celery-Task übernimmt request_id, actor und tenant über Nachrichten-Header (kp_request_id, kp_actor, kp_tenant — nur die Pseudonyme, nie Schlüssel) und bindet sie zusammen mit task_id an jede seiner Logzeilen; nach dem Task werden sie wieder gelöst. Die MCP-Audit-Zeile (mcp_audit_log) trägt zusätzlich api_key_ref (gesalzene Referenz auf den verwendeten Schlüssel), client_ip_ref (gekürzt wie R-03), request_id und entity_keys (die Datensatzschlüssel aus den *_key-Feldern der Werkzeug-Eingabe); alle vier stehen im Art.-15-Export (REQ-033 v1.9). Eine explizit gesetzte tenant=-Angabe einer Zeile hat Vorrang. Dashboards, die Anfragen pro Mandant zählen wollen, können jetzt auf tenant= filtern.

  • Sicherheit: Der Home-Assistant-Token, der Pl@ntNet-API-Key und die Apprise-URLs werden verschlüsselt gespeichert (Fernet mit FERNET_KEY, wie schon der OpenWeatherMap-Key; Issue #2113). Bisher lagen sie im Klartext in system_settings bzw. notification_preferences — eine Apprise-URL enthält den Bot-Token oder das Webhook-Geheimnis. Neue Werte werden beim Speichern verschlüsselt, bestehende einmal durch die Migration v0083 beim Start; ein Lesezugriff schreibt nie. Ohne FERNET_KEY (nur mit DEBUG=true startbar) bleiben sie im Klartext, das Log meldet encryption_disabled. Geändert (API): GET/PUT /api/v1/t/{slug}/notifications/preferences liefern Apprise-URLs nur noch als Platzhalter (tgram://****#1: Schema und Position). Schickst du einen Platzhalter unverändert zurück, bleibt die gespeicherte URL erhalten; ein Platzhalter ohne passende gespeicherte URL wird mit 422 (APPRISE_URL_MASK_UNKNOWN) abgelehnt. Der Datenexport (Art. 15) nennt statt der URLs nur noch ihre Anzahl (urls_configured). Behoben: Das Löschen der Home-Assistant-Einstellungen, des globalen OpenWeatherMap-Keys und der Speicher-Einstellungen meldete Erfolg und ließ den gespeicherten Wert stehen. Upgrade-Hinweis: Nach der Migration findet ein älteres Image diese drei Einstellungen nicht mehr (HA-Token und Pl@ntNet-Key fallen auf die Umgebungsvariablen zurück, Apprise sendet nicht); bei einem Rollback trage sie neu ein.
  • BREAKING (API): Teure authentifizierte Routen haben jetzt ein Budget pro Konto (Issue #2109). Datei-Uploads (Anhänge, Pflanzen- und Aufgabenfotos, Schädlingsbilder, CSV-Import inklusive Bestätigung) erlauben RATE_LIMIT_UPLOAD (Standard 30/minute), Inferenz-Routen (CV-Diagnose, Schädlingserkennung, Pflanzenbestimmung, Referenzbeitrag) RATE_LIMIT_INFERENCE (20/minute) und die PDF-Drucke unter /print RATE_LIMIT_EXPORT (20/minute) — jede Route zählt für sich. Darüber antwortet die API mit 429 und dem Header Retry-After, den jetzt jede 429-Antwort trägt. Die Einstellung RATE_LIMIT_GENERAL entfällt: Sie wurde nie gelesen, obwohl die Referenz sie als Limit der allgemeinen Endpunkte beschrieb. Ist sie in deiner Umgebung gesetzt, kannst du sie löschen.
  • BREAKING (API): HEIC/HEIF-Fotos werden nicht mehr angenommen (Issue #2139). Die EXIF-Bereinigung kann sie nicht neu kodieren, deshalb wurden sie samt GPS-Position gespeichert. Ein Upload antwortet jetzt mit 415; die Kamera- und Bestimmungsdialoge der App wandeln Fotos schon vorher in JPEG um. Solange STORAGE_STRIP_EXIF aktiv ist, lehnt der Upload jeden Bildtyp ab, den die Bereinigung nicht entfernen kann — auch wenn ein STORAGE_ALLOWED_MIME_TYPES_<KATEGORIE>-Override ihn wieder zulässt. Bereits gespeicherte HEIC-Fotos bleiben unverändert und werden bei einer Löschung nach Art. 17 weiterhin als skipped protokolliert.
  • BREAKING (API): Bilder über 40 Megapixel werden mit 413 (IMAGE_PIXEL_LIMIT_EXCEEDED) abgelehnt, bevor sie dekodiert werden (Issue #2108). Bisher passierte eine 445 KB kleine PNG mit 12 000 × 12 000 Pixeln den Upload und die EXIF-Bereinigung legte pro Pixel ein Python-Objekt an (gemessen 80 Byte pro Pixel, rund 11,5 GB). Die Grenze gilt für alle Bild-Dekodierungen des Backends; Bestimmung und CV-Diagnose antworten bei einer Pixelbombe jetzt mit diesem Code statt PAYLOAD_TOO_LARGE. Palettenbilder (PNG im P-Modus) behalten ihre Farben, sie kamen bisher schwarz zurück.
  • Geändert (API): Ein fehlendes Vorschaubild stößt seine Erzeugung höchstens einmal in fünf Minuten an statt bei jedem Abruf (Issue #2108). Schlägt die Erzeugung endgültig fehl (zu groß, nicht dekodierbar oder nach drei Versuchen), antwortet GET …/attachments/{id}/thumbnails/{size} mit 404 statt dauerhaft 202; das MCP-Werkzeug für Tagebuchfotos meldet das Foto dann als unavailable.
  • Sicherheit: Das Sicherheits-Audit security_audit_log hält jetzt auch fest, wenn ein Platform-Admin ein Konto deaktiviert, reaktiviert oder dessen E-Mail-Bestätigung setzt oder zurücknimmt (eine Zeile je tatsächlich geändertem Flag), wenn ein Platform-Admin einen Mandanten sperrt oder reaktiviert und wenn die Verwaltung eines Mandanten oder ein Platform-Admin dessen Löschung beauftragt oder abbricht (Issue #2111, MT-014, NFR-011 R-38, REQ-024 AK-60). Bisher blieb davon keine Zeile; der Abbruch einer Löschung entfernte sogar den Löschnachweis. Die Zeilen nennen nur Konto- und Mandantenschlüssel; eine Kontozeile hat keinen tenant_key, eine Mandantenzeile keinen target_user_key — beide Felder sind in GET /api/v1/admin/platform/security-audit deshalb jetzt nullbar. Eine Änderung von Name, Beschreibung oder Mitgliederlimit eines Mandanten schreibt keine Zeile. Keine Migration.
  • Sicherheit: Jede Änderung einer Mitgliedschaft (hinzufügen, Rolle oder Scopes ändern, entfernen, Austritt, Annahme einer Einladung, Anlegen eines Mandanten) schreibt jetzt eine Zeile in das persistente Sicherheits-Audit security_audit_log (Issue #2111, MT-014, NFR-011 R-38, Migration v0082): wer, wessen Mitgliedschaft, welcher Mandant, alte und neue Rolle bzw. Scopes, Anfrage-ID und Zeitpunkt — nur Konto- und Mandantenschlüssel, kein Freitext. Aufbewahrung 730 Tage (täglicher Task security_audit.purge_expired); bei einer Kontolöschung werden beide Kontoschlüssel zu Tombstone-Hashes. Platform-Admins lesen die Zeilen über GET /api/v1/admin/platform/security-audit. Der Service-Aufruf change_member_scopes verlangt dafür den handelnden Account (actor_user_key).
  • Sicherheit (API): POST /api/v1/admin/platform/tenants/{key}/members und POST /api/v1/admin/platform/users/{key}/memberships verlangen jetzt den eigenen Step-up des Administrators (Issue #2106, MT-008, REQ-024 AK-59): der Körper trägt current_password (oder step_up_token / step_up_code für die Aktion admin_membership_add, gebunden an <tenant_key>|<user_key>); ohne ihn 401, von einem API-Schlüssel 403, nach Fehlversuchen 429. Ein Plattform-Admin kann sich dem Mandanten platform nicht selbst hinzufügen (422), und die Rolle lead im Mandanten platform vergibt nur, wer sie hält. Zuvor genügte die Admin-Sitzung, um einen beliebigen Account — den Mandanten platform mit lead eingeschlossen — ohne Spur hinzuzufügen. Die beiden Admin-Seiten (Mandant, Nutzer) fragen den Step-up beim Hinzufügen ab.
  • BREAKING (API): Eine E-Mail-Einladung nimmt nur noch das Konto an, das die eingeladene Adresse trägt und bestätigt hat (Issue #2115, MT-018, REQ-024 AK-61). POST /api/v1/tenants/invitations/accept antwortet mit 403, wenn das annehmende Konto eine andere Adresse hat oder seine Adresse nicht bestätigt ist; die Einladung bleibt offen, es entsteht keine Mitgliedschaft. Zuvor genügte der Token: ein weitergeleiteter oder abgefangener Link verlieh jedem angemeldeten Konto die Mitgliedschaft, bis zur Rolle lead. Einladungslinks bleiben unverändert für jedes angemeldete Konto gültig. Wer die Einladung mit einem anderen Konto annehmen will, lässt sie an die Adresse dieses Kontos neu ausstellen.
  • Sicherheit: Wer einen Tenant verlässt oder entfernt wird, bleibt dort nicht mehr Zuständiger der Aufgaben, und die Pflege-Erinnerungen gehen nur noch an aktive Mitglieder (Issue #2114, MT-017, REQ-024 AK-62). Zuvor behielten Aufgaben den Ex-Mitglied-Zuständigen, und notifications.dispatch_due_care benachrichtigte ihn weiter mit Pflanzennamen und Fälligkeiten. Die Tageszusammenfassung (notifications.send_daily_summary) fasst jetzt je (Nutzer, Tenant) zusammen — sie legte bisher alle Aufgaben eines Nutzers unter den Tenant seiner ersten Aufgabe.
  • Sicherheit (API): Ein deaktivierter Tenant sperrt seine Mitglieder jetzt wirklich aus (Issue #2105, MT-007, REQ-024 AK-48). Bisher verschwand er nach PATCH /api/v1/admin/platform/tenants/{key} mit is_active=false nur aus der Tenant-Liste; wer den Slug kannte, las und schrieb über /api/v1/t/{slug}/… und den Header X-Active-Tenant weiter (gemessen auf ArangoDB 3.12: 200). Jetzt antworten beide mit demselben 403 wie für einen unbekannten Slug, ein Aufruf ohne Header fällt bei deaktiviertem persönlichem Tenant auf den globalen Katalog zurück. Über MCP war ein deaktivierter Tenant schon vorher nicht erreichbar (list_tenants führt ihn nicht, ein Werkzeugaufruf antwortet not_found); das belegt jetzt ein Test. Die Mitgliedschaften bleiben gespeichert: Reaktivieren stellt den Zugriff unverändert wieder her. Die Plattform-Administration unter /api/v1/admin/platform/… erreicht einen deaktivierten Tenant weiterhin.
  • Ein Mandant entsteht jetzt atomar mit seiner Gründer-Mitgliedschaft, beiden Kanten und der Zeile im Sicherheits-Audit — eine Transaktion statt vier einzelner Schreibvorgänge (Issue #2118, MT-022, REQ-024 AK-63). Zuvor ließ ein Fehler zwischen zwei Schreibvorgängen einen verwaisten Mandanten zurück (Kurzname belegt, niemand kann ihn löschen); eine Registrierung, deren Mandant-Schreibvorgang scheiterte, hinterließ ein Konto samt Anmeldeanbieter ohne persönlichen Mandanten, das die Wiederholung als „schon registriert" beantwortete. Schlägt jetzt ein Schritt der Registrierung fehl, wird das Konto zurückgenommen, und die Wiederholung ist eine echte Registrierung.
  • Sicherheit (MCP): set_nutrient_plan_phase_targets mit dry_run=true las die Phasen-Einträge des Nährstoffplans eines fremden Mandanten (Vorschau before, EC-Werte, Phasennamen), weil get_phase_entries den Plan ohne Mandanten nachschlug (Issue #2104). Der Plan wird jetzt zuerst unter dem Mandanten des Aufrufers aufgelöst; ein fremder oder globaler Plan antwortet mit not_found. get_phase_entries verlangt dafür tenant_key.
  • BREAKING (API, MCP): Mandantenlose MCP-Schreib-Werkzeuge verlangen die Plattform-Admin-Rolle (Issue #2103, REQ-033 §4.4). Bisher genügte jedem kp_-Key die Leitung im eigenen persönlichen Mandanten, sodass jeder Key-Inhaber über assign_species_phase_sequence die Phasenfolge einer globalen Art umhängen konnte. Der Aufruf endet jetzt mit permission.denied (Audit-Zeile DENIED), tools/list zeigt das Werkzeug nur noch Plattform-Admins, und ein auf einen Mandanten beschränkter Key ist nie Plattform-Admin. Das Werkzeug lehnt außerdem eine mandanteneigene Art mit not_found ab. Betrifft nur Betreiber, die das Werkzeug mit einem Nicht-Admin-Key aufgerufen haben.
  • Sicherheit (API): Die Rückwärts-Abfragen GET /api/v1/phase-sequences/{key}/species und GET /api/v1/phase-definitions/{key}/species sowie das MCP-Werkzeug list_species_by_phase_sequence nannten bisher Namen privater Arten aller Mandanten (Issue #2102). Sie liefern jetzt nur noch globale, eigene und freigegebene Arten; das Werkzeug nimmt dafür das optionale Argument tenant (ohne: nur globale Arten).
  • BREAKING (API): Geteilte Workflow-Vorlagen (tenant_key == "", etwa die generierten Aktivitätspläne einer Art) lassen sich nicht mehr an Ort und Stelle ändern oder löschen (Issue #2101, REQ-006 §4, REQ-049 §2.3). PUT /api/v1/t/{slug}/tasks/workflows/{key} legt dem Mandanten eine eigene Kopie an und ändert diese; DELETE auf die geteilte Vorlage sowie das Schreiben ihrer Phasen und Task-Templates über /tasks/phases/* und /tasks/templates/* antworten mit 403. Dasselbe gilt für Task-Templates ohne Eigentümer, die Migration v0035 nicht zuordnen konnte. Wer sie bisher bearbeitet hat, dupliziert die Vorlage zuerst.
  • BREAKING (API): Globale (geteilte) Düngemittel sind nur noch vom Plattform-Administrator änderbar und löschbar (Issue #2100, REQ-049 §2.3). PUT/DELETE /api/v1/t/{slug}/fertilizers/{key} auf ein Düngemittel mit tenant_key == "" antworten für Mandanten-Rollen mit 403, für ein Düngemittel eines anderen Mandanten mit 404; das eigene Düngemittel bleibt für Gärtner änderbar, löschbar nur durch die Leitung. Wer bisher geteilte Düngemittel über einen Mandanten gepflegt hat, braucht dafür die Plattform-Rolle. Dieselbe Regel gilt seitdem aus einer Quelle für Substrate und Düngemittel.
  • Sicherheit: Der E-Mail-Kanal der Benachrichtigungen sendet nur noch an Adressen, deren Inhaber sie belegt hat (email_confirmed_at, Issue #1948, REQ-023 v1.38, REQ-030 v1.7). Ein mit REQUIRE_EMAIL_VERIFICATION=false registriertes Konto bekommt keine Benachrichtigungs-Mail, bis es einen Bestätigungslink angeklickt hat — auch nicht nach dem Umstellen auf true; POST /auth/resend-verification stellt den Link auch solchen Konten aus. Bestehende verifizierte Konten behalten ihre Mail (Migration v0080 stempelt sie; Trockenlauf zählt sie). Beim Start warnt email_channel_without_verification, wenn ein Versender konfiguriert und die Bestätigung aus ist.
  • BREAKING (API): Die Konto-Löschung durch einen Plattform-Admin ist asynchron (Issue #1949, REQ-024 §1a.2, REQ-025 AK-IE-01). DELETE /api/v1/admin/platform/users/{key} antwortet jetzt mit 202 Accepted und einem Body {erasure_key, status, requested_at, message} (zuvor 204 ohne Body). Die Anfrage prüft den Step-up, legt den Löschantrag an, deaktiviert das Konto, widerruft seine Sitzungen und benachrichtigt die anderen Mitglieder seiner persönlichen Gärten sofort (weiterhin ohne Karenzzeit); ein Celery-Task (retention.run_account_erasure) führt danach die Löschung aus — die persönlichen Mandanten in begrenzten Stapeln mit Lebenszeichen, dann den eigenen Plan des Kontos. Die Antworten 500 ERASURE_INCOMPLETE und 502 dieses Endpunkts entfallen — ein fehlgeschlagener oder unvollständiger Lauf ist der Antragsstatus partially_completed (der tägliche Lauf wiederholt ihn), lesbar über das neue GET /api/v1/admin/platform/erasures/{erasure_key}. Weiter synchron entschieden: 401/403/404/422/429 (Step-up, Berechtigung), 409 (ein Lauf hält den Antrag) und 503 (das Deployment kann nicht löschen; nichts geändert). Aufrufer, die auf eine fertige Löschung warteten, müssen den Status abfragen. Löschungen, die die alte synchrone Route vor dem Deploy gestartet hat, enden wie bisher; der tägliche Lauf setzt eine abgebrochene fort
  • Sicherheit (API): PATCH /api/v1/admin/platform/users/{key} verlangt jetzt das eigene Step-up des Administrators, sobald es email_verified oder is_active ändert — auch beim Senken (Issue #1992; zuvor nur beim Anheben). Bisher machte ein einziges PATCH email_verified=false, auch über einen API-Schlüssel, ein etabliertes lokales Konto zum Kandidaten der Bereinigung unbestätigter Konten im nächsten Lauf, die es ohne Step-up und ohne Hinweis an die anderen Mitglieder seines persönlichen Gartens löschte. Ein gesenktes email_verified wird jetzt vermerkt (email_verified_lowered_at), und die Bereinigung entfernt ein so vermerktes Konto nie; sie benachrichtigt außerdem die anderen Mitglieder eines persönlichen Gartens, bevor sie ihn löscht. Konten, die ein Administrator vor diesem Release herabgestuft hat, tragen keinen Vermerk; die Bereinigung verschont zusätzlich jedes Konto, das sich je angemeldet hat (last_login_at) — das deckt sie ab, es sei denn, sie haben sich nie angemeldet
  • BREAKING (API): Die Mandantenlöschung ist asynchron (Issue #1792, REQ-024 AK-53). DELETE /api/v1/tenants/{slug} und DELETE /api/v1/admin/platform/tenants/{key} antworten jetzt mit 202 Accepted und einem Body {tenant_key, status, requested_at, message} (zuvor 200 mit {"message": "Tenant deleted"} bzw. 204 ohne Body). Die Anfrage prüft die Berechtigung und den Step-up, legt den Löschungs-Datensatz an und friert den Mandanten ein (Mitgliedschaften deaktiviert); ein Celery-Task (app.tasks.tenant_tasks.run_tenant_erasure) löscht danach in Stapeln zu je 1000 Datensätzen, jeder in einer eigenen ArangoDB-Transaktion, und meldet zwischen den Stapeln ein Lebenszeichen am Datensatz. Die Fehlerantworten 500 TENANT_ERASURE_INCOMPLETE und 502 der Lösch-Endpunkte entfallen — Fehler des Laufs stehen am Datensatz und werden vom täglichen Lauf wiederholt; ab dem dritten erfolglosen Versuch erscheint das Log-Ereignis tenant_erasure.escalated. Aufrufer, die auf einen abgeschlossenen Löschvorgang warteten, müssen den Mandanten als eingefroren behandeln. Beim Deploy bereits laufende Löschungen übernimmt der tägliche Lauf nach sechs Stunden ohne Lebenszeichen
  • Plant-Instance- und Pflanzdurchlauf-Pflanzen-Responses enthalten eingebettete species- und cultivar-Kurzinfos (Denormalisierung), damit das Frontend lesbare Namen ohne zusätzliche Abfragen bilden kann
  • Ernte: batch_id ist in API-Responses nullbar (string | null) statt einer leeren Zeichenkette; der Eindeutigkeitsindex auf harvest_batches.batch_id ist unique + sparse. Bestandsdaten werden von Migration v0030 angepasst
  • Mandanten: Pflanzen-ID (instance_id) und Chargen-Kennung (batch_id) sind pro Mandant eindeutig, die Stellplatz-ID (slot_id) pro Standort — nicht mehr über alle Mandanten (Issue #2065, Migration v0079). Bisher übersprang das Onboarding beim zweiten Mandanten, der dieselbe Art wählte, alle Pflanzen (onb-<Art>-<n> war vergeben), und eine Kennung, die ein anderer Mandant nutzte, wurde mit 409 abgelehnt — die Ablehnung verriet, dass ein anderer Mandant sie hält. Innerhalb eines Mandanten bzw. Standorts bleibt eine doppelte Kennung ein 409
  • Mandanten: Ein Pflanzdurchlauf folgt der gespeicherten Art eines Eintrags nicht mehr in die private Art eines anderen Mandanten (Issue #1963, Migration v0081). Zeilen aus der Zeit vor den #1871-Korrekturen können nach Migration v0067 (die ihre Kante entfernt hat) noch eine solche Art nennen; das Starten eines solchen Durchlaufs antwortet jetzt mit 404 für die Art (den Eintrag auf eine lesbare Art ändern), Phasenverlauf und Bewässerungsbedarf überspringen sie. Migration v0081 leert außerdem equipment.location_key und die fremden Schlüssel in watering_logs.slot_keys, die auf den Standort oder Stellplatz eines anderen Mandanten zeigen; Standortzuweisungen und Durchlauf-Einträge werden nur gezählt (ihr Feld ist Pflicht) — die Zählungen vor dem Deploy prüfen
  • Mandanten: Eine Pflanze, die ein älterer Pflanzdurchlauf mit der privaten Art eines anderen Mandanten angelegt hat, zeigt den Namen dieser Art nicht mehr an. Pflanzenlisten, Pflanzendetail, Tagebuchübersicht, Druck-Etiketten und -Checklisten, Überwinterungs-Beschriftungen, Foto-Bewertung, Diagnose-Kontext, Gießvorschlag und das MCP-Pflanzendetail lösen die gespeicherte Art jetzt in deinem Mandanten auf: Eine Art, die du nicht lesen darfst, erscheint als unbekannt, nie als fremder Name (Issue #2082). Leser, die sie noch ungeprüft auflösen, stehen im Guard test_stored_species_key_readers_are_decided.
  • Mandanten: Pflege-Dashboard, Winter-Erinnerungen und Pflegeprofil-Vorgaben einer Pflanze, die ein älterer Pflanzdurchlauf mit der privaten Art eines anderen Mandanten angelegt hat, nutzen diese Art nicht mehr: Das Dashboard zeigt für sie keinen Artnamen mehr (vorher den fremden), und Frostempfindlichkeit, Familie und Gießangaben der fremden Art steuern ihre Erinnerungen und Vorgaben nicht mehr (Issue #2082).
  • Pflanzenschutz-Stammdaten: Die Schädlinge, Krankheiten und Behandlungen aus plant_info.yaml und adventskalender.yaml samt ihren Behandlungs-Verknüpfungen werden jetzt angelegt — die Lader lasen bisher Schlüssel, die die Dateien nicht verwenden (u. a. Bemisia tabaci, Septoria apiicola, Ferramol fehlten). Eine Zeile, die das Modell ablehnt, wird übersprungen und protokolliert (ipm_seed_row_invalid), statt Daten zu erfinden; derzeit „Alcohol Spray“ (chemisch ohne Wartezeit)
  • Betrieb (Rollback): Kein Image-Rollback unter die erste Migration eines zurückgezogenen Index (Issue #2064). Ein älteres Backend-Image legt beim Start die eindeutigen Indizes wieder an, die eine Migration entfernt hat, und die Migration läuft danach nie wieder — gemessen auf ArangoDB 3.12.8. Untergrenzen: v0030 (dichter Index auf harvest_batches.batch_id, enthalten ab v0.0.24), v0041 (globaler Index auf species.scientific_name_normalized, ab v0.2.1), v0062 (eindeutiger Index auf attachments.storage_key), v0064 (auth_providers ohne Konfiguration), v0069 (fertilizers mandantenübergreifend), v0075 (tanks.name), v0076 (activities.name), v0077 (workflow_templates.name), v0079 (plant_instances.instance_id, harvest_batches.batch_id, slots.slot_id) — die letzten sieben sind in keinem Release enthalten, ein Rollback unter das nächste Release ist also ausgeschlossen. Jeder Start eines neuen Images entfernt einen so wieder angelegten Index erneut (Log-Ereignis retired_index_reappeared); fehlt sein Ersatz, bleibt er stehen, retired_index_refused_without_replacement erscheint mit Level error und /api/v1/health/ready meldet retired_indexes_refused größer 0 (der Status bleibt ready). Nicht erneut entfernt wird der eindeutige Index auf attachments.storage_key. Ein alter Pod, der nach den neuen startet, legt den Index bis zum nächsten Start eines neuen Pods wieder an. Halten zwei Mandanten schon denselben Namen, startet das alte Image gar nicht (ERR 1210 unique constraint violated)
  • Pflegeerinnerungen: Das Abschließen einer fälligen Gieß-Aufgabe legt die Folgeaufgabe unmittelbar an — zuvor entstand sie erst beim nächtlichen Planungslauf
  • Pflegeerinnerungen: Eine Bestätigung schließt nur noch Pflegeaufgaben, die heute oder früher fällig sind; eine bereits eingeplante Folgeaufgabe bleibt erhalten
  • Mandanten (Härtung, Issue #2119): Jedes Repository, dessen Datensätze einem Garten gehören, entscheidet jetzt ausdrücklich über seine Mandantenbindung. Zwölf weitere — Anhänge, Pflanzentagebuch, Benachrichtigungen, Kalender-Feeds, Schädlingserkennungen, Schädlingsbilder, Pflanzenerkennungen, KI-Anbieter, Einladungen, Mitgliedschaften, Standort-Zuweisungen und Löschprotokolle — lehnen eine Listenabfrage ohne Garten ab, statt die Zeilen aller Gärten zu liefern. Arten, Substrate und Aktivitäten bleiben bewusst ausgenommen (globaler Katalog plus eigene Einträge) und nennen ihren Grund; der Guard test_tenant_bearing_repositories_declare_their_scope hält das fest. Für dich ändert sich nichts.
  • Kalender-Feeds (Issue #2119): Bearbeitest du einen Kalender-Feed (Name, Filter, aktiv), bleibt er deinem Garten und deinem Konto zugeordnet — vorher verlor er beim Speichern seine Zuordnung, verschwand aus deiner Feed-Liste und ließ sich nicht mehr öffnen. Bearbeiten, Löschen und Token-Erneuern prüfen den Besitz jetzt selbst und antworten bei einem fremden Feed mit „nicht gefunden“.
  • Datenschutz: Der persönliche Datenexport (Art. 15/20) enthält jetzt auch die Messwerte und Protokolle deines persönlichen Gartens (Issue #2165): Tanks mit Messungen, Befüllungen und Wartungen, Gieß- und Düngeprotokolle, die Sensoren und ihre Messwerte (einzelne Werte 90 Tage, Stundenmittel 2 Jahre, Tagesmittel 5 Jahre). Ein Abschnitt mit mehr als 100.000 Messwerten enthält die neuesten und nennt die Zahl der ausgelassenen. Messwerte, die die Home-Assistant-Anbindung vor dem 4. Oktober 2026 ohne Gartenbezug gespeichert hat, werden über die Sensoren deines Gartens zugeordnet. Das Export-Format steigt auf Version 1.2: Jeder Abschnitt nennt records_omitted und omission_note.
  • Behoben (MCP, Issue #2169): Ein MCP-Idempotenz-Datensatz, dessen 24 Stunden abgelaufen sind, gilt beim Nachschlagen in der Datenbank nicht mehr als Treffer, auch wenn der stündliche Aufräumlauf ihn noch nicht gelöscht hat. Ein Datensatz mit fehlendem oder unlesbarem Ablaufzeitpunkt zählt als abgelaufen; bisher brach ein unlesbarer Ablaufzeitpunkt den Werkzeugaufruf mit einem Fehler ab.
  • Behoben (KI-Glossar, Issue #2169): Nach jedem abgeschlossenen Neu-Einlesen der Wissensbasis (ai.knowledge_service_ingest, sonntags 03:00 UTC) leert Kamerplanter den Glossar-Cache und erzeugt die Erklärungen aller kuratierten Begriffe neu (REQ-035 §4.3). Bisher stieß nichts diese Kette an: Die KI-Erklärungen liefen nach ihren 7 Tagen ab und wurden nie neu erzeugt, sodass jeder Begriff dauerhaft nur noch den redaktionellen Kurztext zeigte. Das Leeren erfasst jetzt auch den Redis-Cache, aus dem zuerst gelesen wird — sonst hätte die Neuerzeugung jede Variante für noch gültig gehalten. Kosten: ein LLM-Aufruf je Begriff, Sprache und Erfahrungsstufe pro Woche (derzeit 32 × 6 = 192); mit ausgeschalteten KI-Funktionen (ai_features_enabled) entfällt die Neuerzeugung.

Deployment (Helm)

  • Behoben (Helm, Docker): Uploads über 1 MiB scheiterten am Frontend-nginx mit 413 (Issue #2144, Audit-Lücke G-10). Weder die nginx-Konfiguration des Images noch die, die das Chart darüber legt, setzte client_max_body_size; nginx' Standard von 1 MiB lehnte so jeden größeren Anhang, jedes größere Pflanzen- und Aufgabenfoto und jeden größeren CSV-Import ab, lange vor der Backend-Grenze von 25 MiB (gemessen im nginx-unprivileged-Image: 2 MiB und 25 MiB → 413). Beide Konfigurationen nehmen jetzt Bodys bis storage.maxFileSizeMb + 1 MiB an (Standard 26m); alles darüber — auch ein JSON-Body — lehnt nginx mit 413 ab. Das Chart setzt am Ingress main dieselbe Grenze als nginx.ingress.kubernetes.io/proxy-body-size (ingress-nginx schneidet sonst ebenfalls bei 1 MiB ab). Upgrade-Hinweis: ingress.main hat jetzt Standardwerte im Chart; der Ingress entsteht weiterhin nur, wenn du hosts angibst, und enabled: false schaltet ihn ab. Die frühere Doku-Empfehlung proxy-body-size: "10m" war zu klein für 25-MiB-Anhänge
  • Behoben (API): Eine Datei mit genau der erlaubten Größe wurde schon vor dem Lesen mit 413 abgelehnt (Issue #2144). Die Vorprüfung über Content-Length verglich den ganzen Multipart-Body — Datei plus Trennzeilen und Formularfelder — mit der Datei-Grenze. Sie lässt jetzt 64 KiB für diese Hülle zu (eine gemeinsame Prüfung für Anhänge, Pflanzen- und Aufgabenfotos, Schädlingsbilder, Referenzbilder und CV-Diagnose); die Grenze für die Datei selbst bleibt exakt, ein Byte mehr ergibt weiterhin 413.
  • Doku / Dev-Chart: Die Ingress-Beispiele (Helm, ArgoCD, Kubernetes) und values-dev.yaml leiteten /api direkt auf den Backend-Service. Die NetworkPolicy des Backends lässt aber nur das Frontend zu (Issue #1159), und nur über das Frontend-nginx gelten Upload-Grenze und TRUSTED_PROXY_HOPS: "1". Alle Beispiele führen jetzt / einschließlich /api zum Frontend; im Dev-Release leitet Vite /api weiter.
  • Behoben (Helm): Ein Rolling Update blieb auf einem Cluster mit mehreren Nodes hängen, wenn der neue Backend-Pod auf einem anderen Node landete als das ReadWriteOnce-Anhänge-Volume (Multi-Attach error); ein Worker auf einem anderen Node als das Backend startete gar nicht (Issue #2153). Backend und Celery-Worker verlangen jetzt per podAffinity denselben Node, solange die Anhänge auf einem ReadWriteOnce-Volume liegen; das Rolling Update bleibt ohne Unterbrechung. Mit storage.backend: s3 oder ReadWriteMany gibt es keine Einschränkung.
  • Behoben (Helm): Der Celery-Worker hat eine Startup- und eine Readiness-Probe (Issue #2154). Bisher galt er beim Containerstart als bereit, und ein Rollout ersetzte den alten Worker auch dann, wenn der neue sich nicht bei ArangoDB anmelden konnte und jede Aufgabe scheiterte. Jetzt prüft der Worker (WORKER_READY_FILE, vom Chart gesetzt) seine Anmeldung vor der ersten Aufgabe, beendet sich nach 60 Sekunden ohne Erfolg und meldet sich erst bereit, wenn sein Consumer läuft; der Rollout behält bis dahin den alten Worker. Die Liveness-Probe fragt nur noch den eigenen Worker — bisher gelang sie, solange irgendein Worker im Cluster antwortete.
  • Geändert (Betrieb): Celery verteilt Aufgaben auf drei Warteschlangen (Issue #2128, MT-032): critical (Aufbewahrung und Löschung, Datenschutz-Aufräumläufe, Benachrichtigungen, Frostwarnung, Aktoren), celery (Standard, Name unverändert) und bulk (Datensatz- und Bilderfassung, Anreicherung, Glossar-Vorwärmen, Storage-Migration). Jede Aufgabe hat ein Zeitlimit als Notbremse (30 Minuten weich, 35 hart; fortsetzbare oder bewusst gestartete Läufe drei Stunden), jeder Worker-Prozess reserviert nur noch eine Nachricht im Voraus, und das Valkey-visibility_timeout liegt mit vier Stunden über dem längsten Limit, damit eine reservierte Nachricht nicht ein zweites Mal ausgeliefert wird. Der Chart startet den Worker mit -Q critical,celery,bulk. Upgrade-Hinweis: Wer die Worker-args überschreibt, übernimmt die Queue-Liste oder lässt -Q weg; eine Queue ohne lesenden Worker behält ihre Aufgaben stillschweigend. Reihenfolge beim Rollout: Worker zuerst, dann Beat und API — eine ältere API schickt alles weiter an celery, das der neue Worker liest. Noch offen aus MT-032: der N+1-Zugriff in check_auto_transitions (derzeit nicht eingeplant).
  • Hinzugefügt (Betrieb): Ein Worker pro Queue (Issue #2128, MT-032): Mit celeryWorkers.dedicatedQueues: true startet der Chart celery-worker nur noch für critical und zwei Kopien davon — celery-worker-default für celery, celery-worker-bulk für bulk — beim Rendern aus den zusammengeführten Worker-Values abgeleitet — Overlays auf controllers.celery-worker und networkpolicies.celery-worker (Image, Umgebung, securityContext, Egress) gelten für alle drei, und jede Policy, die den Worker zulässt, lässt auch die Kopien zu. Standard bleibt ein Worker für alle drei Queues; das Rendering ohne den Schalter ist unverändert. Mit dem Standard-Volume ReadWriteOnce laufen beide Kopien zusätzlich auf dem Node des Backends (je 512 Mi angefordert). Idempotente Aufräumläufe (Aufbewahrung, abgelaufene Tokens und Einladungen, Audit- und Cache-Einträge, abgelaufene Overrides) bestätigen ihre Nachricht jetzt erst nach dem Lauf (acks_late), damit ein verschwundener Worker-Pod den Lauf nicht verliert. Die Pflege-Benachrichtigungen (notifications.dispatch_due_care, notifications.send_daily_summary) lesen nicht mehr alle Aufgaben aller Mandanten in den Worker, sondern fragen je aktivem Mandanten nur dessen offene, fällige Pflegeerinnerungen per AQL ab — der Speicherbedarf des Workers hängt nicht mehr von der Gesamtzahl der Aufgaben ab; scheitert das Lesen eines Mandanten, wird er protokolliert und übersprungen, die übrigen erhalten ihre Erinnerungen. Doku: Helm — Ein Worker pro Queue.
  • Geändert (Betrieb): Die Beat-Aufgaben für Aktoren, Tanks und Pflanzen lesen nicht mehr die Daten aller Mandanten in den Worker (Issue #2128, MT-032). evaluate_control_rules, sync_actuator_states, sync_tank_states_from_ha, check_tank_alerts und evaluate_quarter_climate holen zuerst die Mandanten, die sie bearbeiten dürfen, und lesen dann nur deren Zeilen — Mandant, entfernte Pflanzen und fehlende Home-Assistant-Entität sind Teil der AQL-Abfrage — seitenweise zu je 1000 Zeilen. Der Worker hält damit eine Seite statt der ganzen Installation, und die Zeilen gesperrter oder zur Löschung vorgemerkter Mandanten werden gar nicht erst gelesen. Dasselbe gilt für die derzeit nicht eingeplanten Aufgaben check_auto_transitions, check_dormancy_triggers und update_vernalization_progress; die beiden letzten bearbeiten dadurch auch keine Pflanzen inaktiver Mandanten mehr. Ohne Mandant (tenant_key leer): Ein Aktor ohne Mandant wird im Regelkreis jetzt wie das Gerät eines gesperrten Mandanten behandelt — eine auslösende Sicherheitsregel entscheidet, sonst wird ein Gerät, das im Notfall abgeschaltet wird (z. B. eine Heizung), einmal ausgeschaltet; Zeitpläne und Overrides wirken nicht mehr. Bisher lief er wie ein Gerät eines aktiven Mandanten mit. Jeder Lauf mit solchen Geräten meldet actuator_control_tenantless_devices mit ihrer Anzahl. Ein Tank ohne Mandant wird weiter auf Alarme geprüft; Tank- und Aktor-Abgleich mit Home Assistant sowie die Pflanzen-Aufgaben lassen Zeilen ohne Mandant aus (Home Assistant las für sie ohnehin nichts). Ein Dokument, das die Validierung nicht besteht, wird übersprungen und mit seinem Schlüssel protokolliert (tenant_walk_skipped_invalid_document), statt den Lauf abzubrechen. Die Log-Zähler tanks_of_closed_tenants entfallen.
  • Upgrade-Hinweis (Helm): Der Block storage in den Chart-Values wirkt jetzt (Issue #2124, NFR-013 v1.9). Bisher las das Chart ihn nicht — storage.backend: s3 lieferte trotzdem local-fs mit PVC aus. Das Chart leitet daraus jetzt die STORAGE_*-Variablen von Backend und Celery-Worker, das PVC backend-attachments (nur bei local-fs) und dessen Mounts ab; die S3-Credentials kommen als genau zwei Schlüssel aus dem Secret in storage.s3.credentialsRef (Default storage-s3-credentials mit STORAGE_S3_ACCESS_KEY_ID/STORAGE_S3_SECRET_ACCESS_KEY). Eigene env-Einträge in deinen Values überschreiben die abgeleiteten Werte weiterhin. Neu verweigert: Mehr als eine Replica von backend oder celery-worker auf dem Standard-Volume ReadWriteOnce bricht helm template/helm upgrade/den ArgoCD-Sync mit einer Meldung ab, weil ein zweiter Pod auf einem anderen Node nie startet (Multi-Attach error). Ausweg: storage.backend: s3 (Pflicht für geteilten Betrieb), storage.localFs.pvc.accessMode: ReadWriteMany mit RWX-StorageClass, oder auf einem Cluster mit genau einem Node storage.localFs.singleNode: true. Das Anhang-PVC trägt zusätzlich argocd.argoproj.io/sync-options: Prune=false,Delete=false, damit ArgoCD es nach dem Wechsel auf S3 nicht löscht — migriere die Dateien vorher mit python -m scripts.storage.migrate --from local-fs --to s3 --checksum-verify
  • Neu (Helm): ArangoDB-Sicherung im Chart (Issue #2122, NFR-002 v2.2). Mit backup.enabled: true und backup.s3.bucket legt das Chart den CronJob kamerplanter-arangodb-backup an: arangodump der Anwendungsdatenbank, Upload per rclone nach s3://<bucket>/<prefix>/<Zeitstempel>/, <prefix>/LATEST als ablesbarer Wiederherstellungspunkt, serverseitige Verschlüsselung und Aufbewahrung (retentionDays, der neueste Dump wird nie gelöscht). Default aus — bestehende Installationen ändern sich nicht. Die Wiederherstellung in einen leeren Namespace beschreibt die neue Seite „Backup & Wiederherstellung“; sie wurde mit einem echten Dump durchgespielt
  • Behoben (Helm): Der Backend-Container startet unter einem startupProbe (Issue #2125, NFR-002 v2.3). Bisher beendete die Liveness-Probe ihn rund 35 s nach dem Start — vor dem ersten Lauschen, solange Migrationen und Seeds noch liefen (gemessen auf einer leeren Datenbank: rund 40 s bis zum Start); eine wartende Replica bekam die vorgesehenen bis zu 600 s Wartezeit auf den Migrations-Lock nie. Der neue Probe gibt dem Start bis zu 900 s; Liveness und Readiness setzen erst danach ein
  • Sicherheit / Upgrade-Hinweis (Helm): Der ArangoDB-Pod erhält nur noch ARANGO_ROOT_PASSWORD statt des ganzen kamerplanter-secrets (Issue #2126, NFR-002 v2.4) — bisher lagen JWT_SECRET_KEY, FERNET_KEY, INTERNAL_SERVICE_TOKEN und alle Mail-/API-Schlüssel im Datenbank-Pod. Backend, Celery-Worker, Celery-Beat und das Backup verbinden sich nicht mehr als root, sondern mit dem Konto kamerplanter (database.arangodb.appUsername), das ein neuer Container app-user im ArangoDB-Pod bei jedem Start mit ARANGODB_PASSWORD anlegt — Lese-/Schreibrecht nur auf der Anwendungsdatenbank, kein Zugriff auf _system. Das Backend kann dafür mit einem solchen Konto starten (bisher brach es mit 401 No read access ab, weil es zuerst die Datenbankliste über _system las). Wer in seinen Values ARANGODB_USERNAME: root oder ein envFrom am ArangoDB-Container setzt (frühere Doku-Beispiele), behält den Root-Zugang, bis er beides entfernt; database.arangodb.appUsername: root stellt das alte Verhalten ausdrücklich her. Empfohlene Folgeschritte: eigenes ARANGODB_PASSWORD und das Root-Passwort in ein eigenes Secret (database.arangodb.rootPasswordSecret) — siehe „Datenbank-Zugänge“ im Kubernetes-Deployment
  • Neu (Helm): Autoscaler, Disruption-Budget und ExternalSecret als Opt-in (Issue #2127, NFR-002 v2.6). controllers.<name>.horizontalPodAutoscaler und controllers.<name>.podDisruptionBudget schalten HPA und PDB pro Controller zu; externalSecrets.app.enabled: true lässt den External Secrets Operator kamerplanter-secrets aus deinem Secret-Store schreiben. Ohne diese Einstellungen rendert das Chart genau wie bisher. Neu verweigert: ein Autoscaler für Backend oder Worker auf dem ReadWriteOnce-Anhänge-Volume (wie schon mehrere replicas), mehr als ein Pod oder ein Autoscaler für celery-beat, arangodb und vectordb, und ein PDB, das keinen Pod verdrängen lässt (jedes Leeren eines Nodes bliebe hängen) — auch mit maxUnavailable: 0 oder "0%" und mit einem Prozentwert für minAvailable, den Kubernetes auf alle Pods aufrundet. Die Helm-Doku empfiehlt außerdem eine Speichergrenze mit noeviction und AOF für Valkey (nicht als Default) und hält fest, dass externe Datenbanken noch nicht unterstützt sind
  • Doku (Helm): Die Startdauer des Backends ist jetzt dokumentiert (Issue #2125): bis zu 900 s Start, bis zu 600 s Warten auf die Migration eines anderen Pods, 300 s bis ein Migrations-Lock als verwaist gilt — und wie du das Budget für eine sehr lange Migration erhöhst. Migrationen und Seeds laufen weiter beim Start des Backends, ein eigener Migrations-Job ist nicht nötig. Ein Test hält die Tabelle mit Chart und Code gleich, ebenso die Untergrenze für failureThreshold (70: 600 s Wartezeit plus 100 s Aufschlag für den eigenen Start)
  • Behoben / Upgrade-Hinweis (Helm): Valkey behält seine Warteschlange über einen Neustart (Issue #2127, NFR-002 v2.8). Das Chart setzte valkey.dataStorage.size, einen Schlüssel, den das Valkey-Chart nicht kennt — es legte kein Volume an, /data lag auf einem flüchtigen emptyDir, und jeder Neustart verlor die wartenden Aufgaben. Jetzt bekommt Valkey ein eigenes Volume (kamerplanter-valkey, 1 GiB, bleibt bei helm uninstall erhalten), die Strategie Recreate, maxmemory 256mb mit noeviction (ein voller Speicher lehnt Schreibzugriffe ab, statt Aufgaben zu verdrängen), das Append-Only-Log und ein Speicherlimit von 640 MiB (die Render-Prüfung verlangt mindestens das Doppelte von maxmemory). Das Upgrade startet Valkey einmal neu; Aufgaben, die in dem Moment warten, gehen verloren wie bei jedem Neustart bisher — lass die Warteschlange vorher leerlaufen. Ohne Standard-StorageClass bleibt Valkey Pending; nenne dann eine in valkey.dataStorage.className. Für die lokale Entwicklung korrigiert skaffold.yaml denselben Schlüssel.
  • Neu (Helm): TimescaleDB als Controller im Chart, standardmäßig aus (Issue #2127, NFR-002 v2.8). controllers.timescaledb.enabled: true schaltet Datenbank, Service, NetworkPolicy und TIMESCALEDB_ENABLED bei Backend, Worker und Beat gemeinsam ein. Das Passwort kommt aus dem Schlüssel TIMESCALEDB_PASSWORD in kamerplanter-secrets, nie aus den Values; die Datenbank läuft ohne Root-Rechte mit schreibgeschütztem Dateisystem. Der auskommentierte Beispielblock mit POSTGRES_PASSWORD: changeme ist entfernt. Init-Container von Frontend und Valkey haben jetzt ebenfalls Ressourcen-Limits.
  • Neu (Helm): Externe ArangoDB und externes Valkey (Issue #2127, NFR-002 v2.9). database.arangodb.external.enabled: true schaltet die mitgelieferte ArangoDB ab und verbindet Backend, Worker, Beat und Backup mit host, port und scheme (http oder https); valkey.enabled: false mit database.valkey.external.url tut dasselbe für Valkey. Für beide rendert das Chart eine Egress-NetworkPolicy auf den angegebenen cidr. Das Backend liest das Schema aus der neuen Variable ARANGODB_SCHEME (Standard http, bisher fest eingebaut) und verweigert den Start bei einem anderen Wert oder einer URL in ARANGODB_HOST. Ohne die Schalter rendert das Chart wie bisher, ergänzt nur ARANGODB_SCHEME=http — das Upgrade startet Backend, Worker und Beat deshalb einmal neu
  • Neu (Helm, Betrieb): Worker-Autoscaling nach Queue-Länge als Opt-in (Issue #2128, #2127, NFR-002 v2.10). Das Backend liefert auf seinem Metrik-Port jetzt celery_queue_length (Label queue: critical, celery, bulk) und celery_broker_up — gelesen beim Scrape, höchstens alle 5 Sekunden; ist Valkey nicht erreichbar, fehlen die Warteschlangen-Werte, der Scrape schlägt nicht fehl. celeryWorkers.queueAutoscaling.enabled: true gibt jedem Worker einen HorizontalPodAutoscaler auf dieser Reihe als External-Metrik, ausgewählt nach den Queues, die der Worker liest. Voraussetzung: monitoring.enabled: true und ein Prometheus Adapter im Cluster, den du selbst installierst — die Regel steht in der Helm-Doku. Widersprüchliche Einstellungen (ohne Monitoring, ohne Metrikname oder Ziel, minReplicas nicht unter maxReplicas, ein zusätzlicher eigener Autoscaler am selben Worker) lehnt das Chart beim Rendern ab; das gilt für minReplicas/maxReplicas und External-Metriken jetzt auch bei jedem handgeschriebenen Autoscaler. Standard: aus, bestehende Installationen bleiben unverändert.

In Entwicklung

  • REQ-025 (Datenschutz/DSGVO): DSGVO Art. 15–21 Betroffenenrechte — spezifiziert, nicht implementiert
  • REQ-027 (Light-Modus): Anonymer Zugang für lokale Instanzen — spezifiziert, nicht implementiert
  • OAuth/OIDC: Vollständige Implementierung (Engine aktuell Stub)
  • TimescaleDB-Integration: Repository und Migrations vorhanden, Feature-Flag gesteuert