# Beute in 3D (Ist-Zustand, Umbau, Rähmchen) — Design Stand: 2026-10-15 · Betrifft: ApiCore-Backend, ApiCore-Frontend · Teilprojekt 1 und 2 von 4 des Inhaber-Wunschs „3D-Beute“ vom 2026-10-15 · Grundlage: Gespräch mit dem Inhaber (Entscheidungen in Abschnitt 1), Holtermann-Produktseiten der eigenen Ausrüstung (Abschnitt 4), Muster aus T3 (Offline-Warteschlange, Stand-Paket), T5 (Tokens, Sonne, Bewegungsstufe) und T6 (Bestandsbuch, Vorbelegung nur sichtbar) ## 1. Ziel Wer ein Volk öffnet, sieht seine Beute als 3D-Modell: durchsichtige Kästen mit blau leuchtenden Kanten, von unten nach oben so gestapelt, wie sie am Stand steht. Das Modell ist der **Ist-Zustand**. Es ändert sich durch Umbau in 3D und durch die Durchsicht, und jede Änderung bleibt als Historie erhalten. - Fährt die Maus über ein Teil, leuchten seine Kanten stärker und es hebt sich leicht. Ein Klick auf eine Zarge lässt sie nach vorn fliegen und größer werden, die anderen Teile weichen zurück und verblassen. Jetzt sieht man die Rähmchen, die vorher nur angedeutet waren. Fährt die Maus über ein Rähmchen, steigt es ein Stück aus der Zarge und zeigt Platz, Art und Namen („R4 · Brutwabe · Königinwabe“). - Rähmchen bekommen eine Art (Brutwabe, Drohnenwabe, Honigwabe, Pollenwabe, Futterwabe, Mittelwand, Leerwabe, Baurahmen, Schied) und optional einen Namen. - Ausrüstung mit Zustand wird sichtbar: „Noch 5 l Zuckerwasser in der Futterzarge“ steht als leuchtender, sanft schwappender Pegel im Modell. Der Ameisensäure-Verdunster zeigt seinen Säurestand. - Neue Zargen, Zwischenböden, Gitter, Folien, Verdunster und Deckel kommen per Drag & Drop hinein; die Bausperre wird für den Winter in den Boden gelegt und wieder entfernt. - Das System kennt Beutensysteme als Daten. Der Inhaber hat heute nur Liebig-Zander mit 10 Rähmchen; Dadant, Deutsch Normalmaß oder Langstroth sind später neue Katalogzeilen, kein neuer Code. Messbar fertig ist dieses Teilprojekt, wenn: 1. Auf der Volk-Seite steht die 3D-Karte oben rechts über „Durchsicht“ und „Varroa“, genau so hoch wie die Karte „Stammdaten“, und das Modell füllt sie ohne Abschnitt aus, egal ob eine Zarge oder sechs. 2. Ein Rähmchen lässt sich in der Karte in drei Schritten umbenennen (Zarge antippen, Rähmchen antippen, Art wählen und speichern). Die Änderung steht in der Historie mit Quelle und Zeit. 3. „Noch 5 l drin, 3 l nachgefüllt“ an der Futterzarge ändert den Pegel im Modell und bietet eine vorbelegte Fütterung an, die erst mit „Speichern“ im Fütterungs-Formular entsteht. 4. Eine Durchsicht kann den Aufbau ändern, auch offline; nach dem Sync sind Durchsicht und Aufbau-Änderung gemeinsam gespeichert oder gemeinsam abgelehnt. 5. Alles, was in 3D geht, geht auch per Tastatur und Screenreader über die Aufbau-Liste. Ohne WebGL erscheint dieselbe Beute als SVG. 6. Bei Bewegungsstufe „ruhig“ oder „aus“ und bei `prefers-reduced-motion` fliegt nichts; Zustände wechseln mit kurzer Überblendung oder sofort. ### Entscheidungen | Frage | Entscheidung | |---|---| | Was zeigt das Modell? | **Den Ist-Zustand.** Er ändert sich durch Umbau (3D oder Liste) und durch die Durchsicht. Jede Änderung ist ein Eintrag in der Historie (Inhaber, 2026-10-15). | | Stil | **Technisch-holografisch** nach Vorgabe des Inhabers: durchsichtige Kästen, blau leuchtende Kanten, Rähmchen zuerst als feine Linien angedeutet, starke Bewegung bei Hover und Klick. Kein Foto-Realismus. | | Platz auf der Seite | **Rechte Spalte ganz oben, über Durchsicht und Varroa**, in derselben Rasterzeile wie „Stammdaten“, damit beide Karten immer gleich hoch sind (Inhaber). | | Größe des Modells | **Automatisch eingepasst:** Die Kamera rahmt das ganze Modell mit Rand. Wenige Zargen erscheinen groß, ein hoher Turm kleiner, nichts wird abgeschnitten (Inhaber). | | Wo wird bearbeitet? | **Gemischt (Inhaber: „C“):** Kleine Änderungen direkt in der Karte (Zarge öffnen, Rähmchen-Art und Name, Füllstände, Bausperre rein/raus). Umbau mit Drag & Drop neuer Teile in der Vollbild-Ansicht „Umbauen“. | | Technik | **three.js mit react-three-fiber, drei und postprocessing (Bloom)** (Inhaber: „A“). Nur damit leuchten Kanten echt, lassen sich Teile in 3D fliegen und Beutensysteme aus Maßen erzeugen. | | Teilprojekte | **Diese Spec: Teil 1 (Beuten-Daten) und Teil 2 (3D-Ansicht).** Teil 3 (KI: Rähmchen-Art aus Foto, Aufbau-Vorschlag beim Anlegen) und Teil 4 (KI-Gespräch in der Durchsicht, baut auf T7) bekommen eigene Specs. Diese Spec legt nur die Schnittstelle an, die sie brauchen: Änderungen mit Quelle `AI_SUGGESTION`, die ein Mensch bestätigt. | | Speicherform | **Aktueller Zustand als geprüftes JSON je Volk plus Änderungs-Protokoll mit Zustand danach.** Ein Aufbau ist ein kleiner Baum (Stapel, Einsätze, Rähmchen); Zeilen je Rähmchen brächten bei jeder Änderung viele Schreibzugriffe und keinen Mehrwert, weil niemand Rähmchen über Völker hinweg relational abfragt. Die Rückverfolgbarkeitskette (CLAUDE.md) berührt das nicht. | | Änderungen | **Als Operationen** (Teil hinzufügen, entfernen, verschieben, Zustand setzen, Rähmchen setzen). Neue Teile bekommen ihre Id im Client, damit offline erstellte Operationen sich aufeinander beziehen können. | | Gleichzeitige Änderungen | **Operationen werden gegen den aktuellen Stand angewandt, nicht gegen die Basis.** Fehlt ein Teil, auf das sich eine Operation bezieht, oder bricht sie eine Regel, wird die ganze Anfrage mit 409 und dem aktuellen Stand abgelehnt. Zustandswerte gelten „wer zuletzt schreibt“. | | Fütterung und Behandlung | **Vorschlag, nie still gebucht.** Ein höherer Futterpegel bietet eine vorbelegte Fütterung an, ein eingesetzter Verdunster mit Säure eine vorbelegte Behandlung (Bestandsbuch T6). Gebucht wird nur mit „Speichern“ im jeweiligen Formular, wie bei T6 und T7. | | Bewegung | **Abweichung von T5 §1 „Keine neue Animation“**, vom Inhaber am 2026-10-15 ausdrücklich gewünscht („extrem animiert“, „moderne Motions“). Gilt hier für die Beute-Ansicht; die übrige App wird in T5 nachgezogen (Abschnitt 12.4). Die Bewegungsstufe des Geräts (`full`/`subtle`/`off`) und `prefers-reduced-motion` gelten immer. | | Altbestand | Bestehende Völker bekommen keinen Aufbau per Migration. Beim ersten Öffnen zeigt die Karte „Aufbau anlegen“ mit einem Startaufbau aus der Vorlage des Beutensystems. Erst „Übernehmen“ legt ihn an. | | Bestehende Felder | `Colony.hiveType` bleibt als Freitext für Völker ohne Aufbau. `Colony.occupiedFrames` bleibt die **Volksstärke** (von Bienen besetzte Waben, aus Durchsichten) und wird vom Aufbau nie geschrieben. Rähmchen-Zahlen der Beute kommen nur aus `deriveHiveFacts` (Abschnitt 5.3). | | Rechte | Lesen mit `colonies:read`, Aufbau ändern mit `colonies:update`, Katalog pflegen mit der neuen Berechtigung `hive-systems:manage` (nur Systemrolle ADMIN). Folge: Die Rolle STAFF darf heute Durchsichten anlegen, aber kein `colonies:update`, also auch die Beute nicht ändern; der Inhaber kann das in der Rollenverwaltung freischalten. | ## 2. Umfang ### 2.1 Im Umfang - Katalog: Beutensysteme, Teile-Arten mit Maßen, Platzregeln und Zustandsfeldern, Vorlagen. Startdaten für Liebig-Zander mit der Ausrüstung des Inhabers (Abschnitt 4). - Aufbau je Volk: anlegen, ändern, Historie, Stand an einem Tag. - API unter `/api/v1/...` für alles davon, mit Rollenprüfung. - Durchsicht: Block „Beute“ im Formular, Änderungen gehen mit der Durchsicht in einer Transaktion, auch über die Offline-Warteschlange. - Frontend: 3D-Karte auf der Volk-Seite, Vollbild „Umbauen“, Aufbau-Liste (Tastatur, Screenreader), SVG-Rückfall, Historie-Ansicht. - Vorschläge für Fütterung und Behandlung aus Zustandsänderungen. - Ereignis `HiveChanged` im Domain-Event-Outbox (ohne Handler in dieser Spec). ### 2.2 Nicht im Umfang - KI-Bewertung von Rähmchen aus Fotos, KI-Vorschlag beim Anlegen (Teil 3). - KI-Gespräch während der Durchsicht (Teil 4). - Bauteil-Inventar (wie viele Zargen liegen im Lager). Das bestehende Modul `equipment` verwaltet Geräte und bleibt unverändert. - Fotos der echten Beuten als Texturen. Der Inhaber schickt Fotos; sie dienen zum Abgleich der Maße und Proportionen, nicht als Bild im Modell. - Mehrere Völker in einer Beute (Zwischenboden mit zwei Völkern). Der Zwischenboden ist ein Teil im Stapel; ein zweites Volk darüber ist ein eigenes Volk mit eigener Beute. ## 3. Begriffe | Begriff | Bedeutung | |---|---| | Beutensystem | Maßsystem einer Beute, z. B. „Zander nach Dr. Liebig, 10 Rähmchen“. Bestimmt Grundfläche, Rähmchenmaß und Rähmchen je Zarge. | | Teile-Art | Eine Katalogzeile, z. B. „Liebig Zander Ganzzarge“ oder „Nassenheider Professional“. | | Teil | Ein Exemplar einer Teile-Art im Aufbau eines Volks, mit eigener Id und eigenem Zustand. | | Stapel | Die Teile, die übereinander liegen, von unten (Boden) nach oben (Dach). | | Einlage | Ein Teil, das in einem Boden liegt (Bausperre). | | Einsatz | Ein Teil, das in einer Zarge steht oder liegt (Verdunster). | | Aufbau | Stapel plus Einlagen, Einsätze und Rähmchen eines Volks. | ## 4. Katalog ### 4.1 Teile-Arten `kind` legt Platz und Darstellung fest. Der Platz ergibt sich aus `kind`: `FLOOR_INSERT` ist eine Einlage im Boden, `EVAPORATOR` ein Einsatz in einer Zarge, alles andere liegt im Stapel. | `kind` | Platz | Darstellung | Regeln | |---|---|---|---| | `FLOOR` | Stapel | Kasten mit Gitterlinien | Genau einer, immer ganz unten. | | `BOX` | Stapel | durchsichtiger Kasten mit Rähmchen | `frameSlots` Plätze (0 = Leerzarge). `boxDepth`: `FULL` oder `SHALLOW`. | | `DIVIDER_BOARD` | Stapel | dünne Platte mit Löchern | Zwischenboden. | | `QUEEN_EXCLUDER` | Stapel | Gitter aus Linien | — | | `FEEDER` | Stapel | flacher Kasten mit Kammern | Zustandsfelder je Kammer (Pegel). | | `COVER_SHEET` | Stapel | sehr dünne Fläche | Abdeckfolie. | | `PROPOLIS_SCREEN` | Stapel | Gitter aus Linien | — | | `INNER_LID` | Stapel | flache Platte | Holzdeckel. | | `INSULATION` | Stapel | Platte mit Punktmuster | Dämmung, z. B. Styropor-Einlage. | | `ROOF` | Stapel | Platte mit Rand, über das Teil darunter gestülpt | Nur ganz oben. | | `FLOOR_INSERT` | Einlage im Boden | Gitterfläche im Boden | Bausperre. Höchstens eine je Boden. | | `EVAPORATOR` | Einsatz in einer Zarge | Flasche mit Pegel auf Docht-Platte | Ameisensäure-Verdunster. Liegt auf den Rähmchen der Zarge darunter, darum meist in einer Leerzarge. Höchstens einer je Zarge. | Alle anderen Reihenfolgen sind erlaubt. Ungewöhnliche Folgen (Absperrgitter direkt auf dem Boden, Dämmung unter der Zarge) sind Hinweise in der Oberfläche, keine Sperre, weil Imker umbauen, wie es die Lage verlangt. ### 4.2 Zustandsfelder Jede Teile-Art trägt ein `stateSchema` als JSON-Liste. Zwei Feldarten: ```json [ { "key": "syrupLiters", "label": "Flüssigfutter", "kind": "level", "unit": "l", "max": 3, "step": 0.5, "feedType": "SUGAR_WATER" }, { "key": "fondantKg", "label": "Futterteig", "kind": "level", "unit": "kg", "max": 2.5, "step": 0.5, "feedType": "FEED_PASTE" }, { "key": "entrance", "label": "Flugloch", "kind": "choice", "options": ["OPEN", "NARROW", "CLOSED"], "optionLabels": ["offen", "eng", "zu"] } ] ``` - `level`: Zahl von 0 bis `max` in Schritten von `step`. Wird im Modell als Pegel gezeigt. - `choice`: einer der Werte aus `options`. - `feedType` (optional, nur bei `level`): Futterart der Fütterung (`FeedType` aus dem Fütterungs-Modul), die Abschnitt 8 vorbelegt. - Fehlt ein Feld im Zustand eines Teils, gilt 0 bzw. die erste Option. - Der Server prüft jeden Wert gegen das Schema der Teile-Art. Unbekannte Schlüssel sind ein Fehler. - Das Schema einer Teile-Art darf sich nur additiv ändern (neue Felder). Ein entferntes Feld bleibt in alten Ständen der Historie lesbar, weil jeder Stand sein JSON vollständig speichert. ### 4.3 Startdaten Liebig-Zander (Ausrüstung des Inhabers) Maße aus den Holtermann-Produktseiten (abgerufen 2026-10-15). Alle Werte sind Katalogdaten und in den Einstellungen änderbar. | Teile-Art | `kind` | Maße (L × B × H, mm) | Zustandsfelder | |---|---|---|---| | Liebig Wanderboden | FLOOR | 525 × 430 × 120 | Flugloch (offen/eng/zu), Varroa-Schublade (drin/draußen) | | Liebig Bausperre | FLOOR_INSERT | 475 × 375 × 5 | — | | Liebig Zander Ganzzarge, 10 Rähmchen | BOX, FULL, 10 Plätze | 525 × 430 × 230 | — | | Leerzarge (Liebig Zander Ganzzarge ohne Rähmchen) | BOX, FULL, 0 Plätze | 525 × 430 × 230 | — | | Liebig Zwischenboden | DIVIDER_BOARD | 525 × 430 × 50 | Spundlöcher (offen/zu/Bienenflucht) | | Niro-Absperrgitter Zander Liebig | QUEEN_EXCLUDER | 478 × 376 × 2 | — | | Liebig Zweikammer-Futterzarge | FEEDER | 520 × 420 × 70 | Flüssigfutter 0–3 l (`feedType` `SUGAR_WATER`), Futterteig 0–2,5 kg (`feedType` `FEED_PASTE`) | | Abdeck-Fix (Folie, klar, 0,4 mm) | COVER_SHEET | 478 × 378 × 1 | — | | Imgut Lotus-Abdeckung (0,5 mm) | COVER_SHEET | 478 × 378 × 1 | — | | Imgut Propolisgitter | PROPOLIS_SCREEN | 478 × 378 × 3 | — | | Liebig Holzdeckel | INNER_LID | 525 × 430 × 35 | Futterloch (offen/zu) | | Styropor-Einlage (selbst gebaut) | INSULATION | 465 × 365 × 20 | — | | Edelstahldeckel konisch Liebig | ROOF | 525 × 430 × 60 (Innenmaß) | — | | Nassenheider Verdunster Professional | EVAPORATOR | 200 × 140 × 120 | Ameisensäure 0–290 ml | Die Startdaten kommen per Migration mit **festen Ids** (`hive-system-zander-liebig-10`, `hive-part-zander-liebig-10-floor`, …), damit die Vorlage stabile Verweise hat und eine Wiederherstellung aus dem Backup in eine frische Installation keine doppelten Namen erzeugt (Abschnitt 5.1). Annahmen, die der Inhaber mit Fotos oder Zollstock bestätigt (Abschnitt 14): Außenmaß der Zarge 525 × 430 mm (abgeleitet aus dem Innenmaß des Edelstahldeckels), Dicke der Styropor-Einlage 20 mm, Maße des Verdunsters. ### 4.4 Beutensystem und Vorlage `HiveSystem` „Zander nach Dr. Liebig, 10 Rähmchen“: Rähmchen 477 × 220 mm (Oberträger × Höhe), 10 Plätze je Ganzzarge, Grundfläche 525 × 430 mm. Die **Vorlage** des Systems ist der Aufbau, mit dem ein neues Volk startet: Wanderboden, Ganzzarge mit 10 Plätzen, Abdeckfolie, Holzdeckel, Styropor-Einlage, Edelstahldeckel. Die Vorlage ist ein Aufbau-JSON wie in Abschnitt 5.2 (ohne Ids, die erzeugt der Server beim Anlegen) und in den Einstellungen änderbar. Die Rähmchen-Arten der Vorlage gelten; der Nutzer korrigiert vor „Übernehmen“. „Wie Volk B3 aufbauen“ kopiert stattdessen den aktuellen Stapel eines anderen Volks mit Zustandswerten 0, ohne Namen und mit allen Rähmchen als `EMPTY_COMB` außer Mittelwänden. ## 5. Datenmodell ### 5.1 Prisma ```prisma model HiveSystem { id String @id @default(cuid()) name String @unique frameWidthMm Int // Oberträger frameHeightMm Int footprintLMm Int footprintWMm Int template Json // Aufbau-JSON ohne Ids, Schema 5.2 active Boolean @default(true) partTypes HivePartType[] setups HiveSetup[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model HivePartType { id String @id @default(cuid()) systemId String system HiveSystem @relation(fields: [systemId], references: [id]) kind HivePartKind name String articleRef String? // z. B. Holtermann-Art.-Nr. lengthMm Int widthMm Int heightMm Int boxDepth HiveBoxDepth? frameSlots Int? // nur BOX stateSchema Json // Abschnitt 4.2 active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([systemId, name]) } model HiveSetup { colonyId String @id colony Colony @relation(fields: [colonyId], references: [id]) systemId String system HiveSystem @relation(fields: [systemId], references: [id]) version Int @default(1) state Json // Abschnitt 5.2, immer vollständig updatedAt DateTime @updatedAt changes HiveChange[] } model HiveChange { id String @id @default(cuid()) colonyId String setup HiveSetup @relation(fields: [colonyId], references: [colonyId]) version Int // Version nach der Änderung occurredOn DateTime // fachlicher Tag (Durchsicht oder Eingabe) source HiveChangeSource inspectionId String? @unique inspection Inspection? @relation(fields: [inspectionId], references: [id]) userId String user User @relation(fields: [userId], references: [id]) ops Json // angewandte Operationen, Abschnitt 6 stateAfter Json note String? createdAt DateTime @default(now()) @@unique([colonyId, version]) @@index([colonyId, occurredOn]) } enum HivePartKind { FLOOR BOX DIVIDER_BOARD QUEEN_EXCLUDER FEEDER COVER_SHEET PROPOLIS_SCREEN INNER_LID INSULATION ROOF FLOOR_INSERT EVAPORATOR } enum HiveBoxDepth { FULL SHALLOW } enum HiveChangeSource { INIT CARD EDITOR INSPECTION AI_SUGGESTION } ``` - Rück-Relationen: `Colony.hiveSetup HiveSetup?`, `Inspection.hiveChange HiveChange?`, `User.hiveChanges HiveChange[]`. - Nichts wird hart gelöscht. Teile-Arten und Systeme werden mit `active = false` ausgeblendet; Aufbauten, die sie nutzen, bleiben gültig. - Anlegen eines Aufbaus schreibt Version 1 und eine `HiveChange` mit Quelle `INIT`, damit die Historie vollständig ist. Ein zweites `POST /colonies/:id/hive` antwortet 409 `HIVE_EXISTS` (geprüft unter Sperre, nicht über einen Unique-Fehler). - **Sperren:** `HiveSetup` kommt in `LOCKABLE_TABLES` direkt nach `Colony`. Jede Aufbau-Schreibung sperrt zuerst die Zeile `Colony` mit `FOR UPDATE` (und prüft dabei, dass das Volk lebt), dann `HiveSetup`. - **Idempotenz:** über den bestehenden `@Idempotent()`-Interceptor (`Idempotency-Key` je Nutzer, T3). Keine eigene Spalte. - **Backup:** Das In-App-Backup listet Modelle von Hand (`backup.service.ts`). Export von `hiveSystem`, `hivePartType`, `hiveSetup`, `hiveChange`; Import in der Reihenfolge `hiveSystem` → `hivePartType` → (nach `colony`) `hiveSetup` → (nach `inspection`) `hiveChange` mit `userId` über `resolveUserId(...)`. Nur versiegelte Sicherungen, nicht in `LEGACY_UNSEALED_TABLES`. Gibt es beim Import für ein Volk schon einen Aufbau, gewinnt die Sicherung: Aufbau und Änderungen dieses Volks werden vorher entfernt (Import ist eine Wiederherstellung, kein Zusammenführen). Der Abdeckungstest des Backups wird ergänzt. ### 5.2 Aufbau-JSON ```ts interface HiveState { schemaVersion: 1; stack: HivePart[]; // Index 0 = unten } interface HivePart { id: string; // UUID, im Client erzeugt partTypeId: string; label?: string; // z. B. „Honigraum 1“, max. 40 Zeichen state: Record; inserts?: HivePart[]; // FLOOR: höchstens eine Bausperre; BOX: höchstens ein Verdunster frames?: HiveFrame[]; // nur BOX, Länge = frameSlots der Teile-Art } interface HiveFrame { slot: number; // 1 … frameSlots, von links mit Blick auf das Flugloch type: FrameType; name?: string; // max. 40 Zeichen } type FrameType = | "BROOD" | "DRONE" | "HONEY" | "POLLEN" | "FEED" | "FOUNDATION" | "EMPTY_COMB" | "DRONE_FRAME" | "DIVIDER" | "NONE"; ``` `NONE` ist ein leerer Platz (Lücke). Deutsche Namen: Brutwabe, Drohnenwabe, Honigwabe, Pollenwabe, Futterwabe, Mittelwand, Leerwabe, Baurahmen, Schied, leer. Höchstens 30 Teile im Stapel. Ein Validator prüft nach jeder Operation das ganze JSON: Schema, Platzregeln aus 4.1, `frames.length == frameSlots`, eindeutige Ids, Zustandswerte gegen `stateSchema`, alle `partTypeId` gehören zum System des Aufbaus. ### 5.3 Abgeleitete Werte Eine reine Funktion `deriveHiveFacts(state, catalog)` liefert: - `boxCount`, `broodFrames` (BROOD), `honeyFrames` (HONEY), `builtFrames` (alles außer NONE, FOUNDATION, DIVIDER), `levels` (alle `level`-Felder mit Teil, Einheit, Futterart und Wert), `hasFloorInsert`, `evaporatorAcidMl`. Diese Werte schreiben nie in `Colony` oder `Inspection`. Sie dienen der Anzeige und als sichtbare Vorschläge (Abschnitt 7). ### 5.4 Ereignis Neuer Eintrag im Ereignis-Katalog `domain-event-types.ts`: `HiveChanged: { colonyId: string; version: number; source: HiveChangeSource; inspectionId: string | null }`, `aggregateType` `Colony`. Der Service schreibt ihn nach jeder Änderung in derselben Transaktion in den Outbox. Keine Inhalte, keine Namen. Nicht in `DOMAIN_EVENT_WEBHOOKS`. Ein Ereignis ohne Handler wird als erledigt abgeschlossen; Handler kommen mit späteren Teilprojekten (z. B. Autopilot „Futter fast leer“). Die Typ-Liste in `domain-events.service.spec.ts` und der Test „nicht weitergeleitet“ in `webhook.handlers.spec.ts` werden ergänzt. ## 6. Operationen und API ### 6.1 Operationen ```ts type HiveOp = | { op: "addPart"; part: HivePart; at: number } // Stapel-Index | { op: "addInsert"; hostId: string; part: HivePart } | { op: "removePart"; id: string } // Stapel, Einlage oder Einsatz | { op: "movePart"; id: string; to: number } | { op: "setState"; id: string; key: string; value: number | string } | { op: "setFrame"; boxId: string; slot: number; type: FrameType; name?: string | null } | { op: "setFrames"; boxId: string; frames: HiveFrame[] } | { op: "setLabel"; id: string; label: string | null }; ``` `applyHiveOps(state, ops, catalog)` ist eine reine Funktion. Sie gibt den neuen Zustand oder einen Fehler mit Index der Operation und Grund zurück. Ein `at` außerhalb des Stapels wird auf die nächste gültige Stelle begrenzt (Anfang oder Ende), damit offline erfasste Einfügungen nach einem fremden Entfernen nicht scheitern. Das Backend nutzt die Funktion verbindlich. Das Frontend nutzt eine eigene Fassung für die Vorschau. Beide werden gegen dieselben JSON-Fixtures geprüft (`ApiCore-Backend/test/fixtures/hive-ops/*.json`, Muster wie `placeholder-keys`, im Frontend mit `skipIf`, wenn die Datei im Image-Build fehlt). Höchstens 100 Operationen je Anfrage. ### 6.2 Endpunkte Rechte über den bestehenden `PermissionsGuard` (jede Route braucht eine Berechtigung): | Methode und Pfad | Zweck | Berechtigung | |---|---|---| | `GET /hive-systems` | Systeme mit Teile-Arten | `colonies:read` | | `POST /hive-systems`, `PATCH /hive-systems/:id` | System anlegen, ändern, ausblenden | `hive-systems:manage` | | `POST /hive-systems/:id/part-types`, `PATCH /hive-part-types/:id` | Teile-Art anlegen, ändern, ausblenden | `hive-systems:manage` | | `GET /hive-systems/:id/template` | Vorlage als Aufbau-JSON mit frischen Ids | `colonies:read` | | `GET /colonies/:id/hive` | `{ version, systemId, state, facts }`, 404 ohne Aufbau | `colonies:read` | | `POST /colonies/:id/hive` | Aufbau anlegen: `{ systemId, state, occurredOn }` (vom Client aus Vorlage oder anderem Volk vorbereitet und vom Nutzer bestätigt), `@Idempotent()` | `colonies:update` | | `POST /colonies/:id/hive/changes` | `{ occurredOn, source, ops, note? }`, `@Idempotent()` | `colonies:update` | | `GET /colonies/:id/hive/changes?cursor=&limit=` | Historie, neueste zuerst, ohne `stateAfter` | `colonies:read` | | `GET /colonies/:id/hive/at?day=YYYY-MM-DD` | Stand am Ende dieses Berliner Kalendertags | `colonies:read` | - `hive-systems:manage` kommt in den `PERMISSION_CATALOG`. Die Katalog-Synchronisierung beim Start gibt sie der Systemrolle ADMIN; keine Grant-Migration. - `POST …/changes` antwortet mit dem neuen Stand. - **400** bei Formfehlern: unbekannte `op`, falsche Typen, mehr als 100 Operationen (`@IsArray @ArrayMaxSize(100)` plus Validator). - **409** mit `{ code: "HIVE_CONFLICT", opIndex, reason, current: { version, state } }` nur, wenn eine Operation gegen den aktuellen Stand scheitert (fehlende Id, Regel aus 4.1 verletzt). Der Code unterscheidet die Antwort von den 409 des Idempotenz-Interceptors; Client und Warteschlange verzweigen auf `body.code`. - `source` ist in dieser Spec `CARD` oder `EDITOR`. `INIT` und `INSPECTION` setzt nur der Server. `AI_SUGGESTION` steht im Enum, wird aber erst mit Teil 3 angenommen (vorher 400). - `occurredOn` ist ein Berliner Kalendertag und darf nicht in der Zukunft liegen (wie T6) und nicht vor dem Tag der `INIT`-Änderung. Es darf vor dem Tag der letzten Änderung liegen (Nachtrag). - Es gibt keine `baseVersion`: Operationen wirken immer auf den aktuellen Stand (Abschnitt 1). Die Antwort trägt die neue `version`, damit der Client weiß, ob er aktuell ist. - **Stand am Tag X** ist `stateAfter` der Änderung mit der höchsten Version unter allen Änderungen mit `occurredOn <= X`. Weil Operationen immer auf den aktuellen Stand wirken, enthält ein Nachtrag auch spätere Änderungen. Das ist eine bewusste Näherung; die Historie zeigt Nachträge mit dem Zusatz „nachgetragen am …“. Vor dem `INIT`-Tag antwortet der Endpunkt 404. - Völker, die nicht mehr leben (`!isLivingColonyStatus(status)`), sind schreibgeschützt: 409 wie `assertActive` im Volk-Service. - Audit-Log: Katalog-Änderungen wie alle Stammdaten. Aufbau-Änderungen haben ihr eigenes Protokoll (`HiveChange`) und gehen nicht zusätzlich ins Audit-Log. ## 7. Durchsicht - Das Durchsicht-Formular bekommt den Abschnitt **„Beute“** mit der Karte im Bearbeiten-Modus (dieselbe Komponente wie auf der Volk-Seite). Alle Änderungen sammeln sich als Operationen im Formular-Zustand und erscheinen als Liste „Änderungen an der Beute (3)“ mit „Rückgängig“ je Eintrag. - `CreateInspectionDto` bekommt `hiveChange?: { ops }`. Der Service nimmt `hiveChange` wie `applyToColony` aus dem DTO heraus, bevor die Durchsicht geschrieben wird. Er legt Durchsicht und `HiveChange` (Quelle `INSPECTION`, `occurredOn` = Tag der Durchsicht) in **einer** Transaktion an, mit den Sperren aus 5.1. Scheitert der Aufbau-Teil, scheitert die ganze Anfrage. - **Rechte:** Eine Durchsicht mit `hiveChange` braucht `inspections:create` **und** `colonies:update`, geprüft vor der Transaktion (Muster der bestehenden Zusatzprüfung im Inspection-Service). Fehlt `colonies:update`, antwortet der Server 403 für die ganze Anfrage; das Formular blendet den Abschnitt „Beute“ für solche Nutzer aus und zeigt nur das Modell. - Aus dem Aufbau wird `broodFrames` der Durchsicht vorbelegt, sichtbar als Vorschlag „Laut Beute: 6 Brutwaben“ mit „Übernehmen“, nie still (Muster „Letztes Mal“ aus T3). - **Offline:** Die Durchsicht geht wie heute in `pending-inspections`, mit `hiveChange` im Input; gesendet wird sie über die Beute-Zeitleiste aus Abschnitt 10. Bei 409 `HIVE_CONFLICT` zeigt die Warteschlange „Beute hat sich inzwischen geändert“ mit den Knöpfen „Ohne Beute-Änderung senden“ (gleicher `Idempotency-Key`, den eine gescheiterte Anfrage freigibt) und „Verwerfen“. - Ändern einer gespeicherten Durchsicht (`PATCH`) ändert ihre Beute-Änderung nicht. Wer nachträglich den Aufbau korrigiert, legt in der Karte eine neue Änderung an. ## 8. Fütterung und Behandlung | Auslöser | Vorschlag nach dem Speichern | Vorbelegung | |---|---|---| | `setState` auf einem `level`-Feld eines FEEDER steigt | Hinweis „3 l Flüssigfutter nachgefüllt. Als Fütterung eintragen?“ | Fütterungs-Formular mit Volk, Datum, Menge = Zuwachs, Futterart = `feedType` des Felds | | `addInsert` EVAPORATOR mit Säure > 0, oder Säure steigt | Hinweis „Verdunster mit 250 ml eingesetzt. Als Behandlung eintragen?“ | Behandlungs-Formular mit Volk, Datum, Menge; das Arzneimittel wählt der Nutzer (T6-Regeln, Charge, Bestandsbuch) | | `removePart` EVAPORATOR | Hinweis „Behandlung beenden?“, wenn eine offene Behandlung desselben Volks besteht | Ende-Datum | - Die Logik `hiveFollowUps(opsApplied, before, after)` ist eine reine Funktion im Frontend. - Der Hinweis ist ein Toast mit Aktion und steht zusätzlich als Knopf in der Historie-Zeile („Fütterung eintragen“), damit er nicht verloren geht. - In der Durchsicht erscheinen dieselben Hinweise nach dem Speichern der Durchsicht. - Es wird nie automatisch gebucht: Fütterung und Behandlung sind Nachweise mit eigener Verantwortung (T6). ## 9. 3D-Ansicht ### 9.1 Aufbau der Komponenten ``` components/hive/ hive-card.tsx Karte auf der Volk-Seite (lädt Daten, wählt 3D oder SVG) hive-scene/ alles, was three.js braucht, nur per dynamic() geladen hive-canvas.tsx , Licht, Bloom, Kamera-Einpassung stack-part.tsx ein Teil im Stapel (Kasten, Kanten, Hover, Flug) frame-plate.tsx ein Rähmchen fill-volume.tsx Pegel (Flüssigkeit, Futterteig, Säure) use-hive-motion.ts Ziele für Position, Maßstab, Deckkraft je Zustand hive-svg.tsx Rückfall und Druck, aus derselben Layout-Funktion hive-outline.tsx Aufbau-Liste (Tastatur, Screenreader, Bearbeiten) frame-sheet.tsx Sheet „Rähmchen 4“ (Art, Name) part-sheet.tsx Sheet für Zustandsfelder eines Teils lib/hive/ types.ts, ops.ts (applyHiveOps), layout.ts (hiveLayout), facts.ts, follow-ups.ts, api.ts, labels.ts ``` `hiveLayout(state, catalog)` ist eine reine Funktion: Sie rechnet aus Maßen in mm die Lage jedes Teils, jeder Einlage, jedes Einsatzes und jedes Rähmchens (Mittelpunkt, Größe) und die Gesamt-Box. 3D, SVG und Tests nutzen sie gemeinsam. Maßstab: 1 Einheit = 100 mm. Der Zustand der Ansicht (berührtes Teil, offene Zarge, berührtes Rähmchen) liegt in einem kleinen Store außerhalb von three.js (`useHiveView`), damit Liste, SVG und Szene denselben Zustand teilen. ### 9.2 Platz und Größe - Volk-Seite: Die heutige Zeile `lg:grid-cols-3 items-start` wird in zwei Zeilen geteilt. Zeile 1: Stammdaten (1 Spalte) und Beute-Karte (2 Spalten), **ohne** `items-start`, damit die Rasterzeile beide Karten auf dieselbe Höhe streckt. Die Beute-Karte bekommt über `lg` keine eigene Höhe, sie folgt den Stammdaten; der Canvas sitzt in einem Flex-Kind mit `h-full min-h-0`. Wächst die Stammdaten-Karte (Konflikt- oder Sperrbezirk-Hinweise), wächst die Beute mit und passt neu ein. Zeile 2 behält `items-start`. Zeile 2: Aktionen und QR (1 Spalte), die Karten Durchsicht, Varroa usw. (2 Spalten). Die Reihenfolge im DOM bleibt Stammdaten, Beute, Aktionen, Karten. - Unter `lg` (eine Spalte) hat die Beute-Karte eine feste Höhe von `min(55svh, 520px)`, mindestens 320 px. - **Einpassen:** drei `` um das Modell. Bei Änderung des Aufbaus oder der Kartengröße passt die Kamera neu ein, mit Dämpfung (Bewegungsstufe `full`) oder sofort. Die Kamera schaut leicht von oben-vorn (Höhenwinkel 20°, Seitenwinkel 30°). Im Fokus-Modus (9.4) rahmt sie die gewählte Zarge. ### 9.3 Stil - **Hintergrund der Karte:** immer ein dunkler „Bildschirm“ (eigene Tokens `--holo-bg`, `--holo-edge`, `--holo-edge-strong`, `--holo-fill`, `--holo-label-bg`, `--holo-label-fg` in `:root` und `.dark`; die Sonne-Sätze kommen ab T5). Ein feines Raster am Boden gibt Halt. - **Teile:** Fläche mit `MeshBasicMaterial`, Deckkraft 0,06–0,12, additiv. Kanten mit drei `` in `--holo-edge`. Bloom aus `@react-three/postprocessing` mit hoher Helligkeitsschwelle, sodass nur Kanten, Pegel und gewählte Teile leuchten. - **Rähmchen:** im Ruhezustand nur Oberträger und Umriss als Linien (Deckkraft 0,25). Im Fokus-Modus als Platten mit Kante und einer Tönung je Art. Die Art steht immer auch als Wort im Etikett und in der Liste (T5: Zustand nie nur über Farbe). - **Pegel:** durchsichtiges, leuchtendes Volumen mit einer leicht wogenden Oberfläche (Vertex-Verschiebung mit `sin`, nur bei Bewegung `full`). Etikett „5 l / 3 l“. - **Etiketten:** drei `` mit DOM-Text in `--holo-label-*` (scharf, übersetzbar, gut lesbar). Höchstens drei Etiketten gleichzeitig. - **Sonne-Modus (ab T5, solange es ihn nicht gibt, entfällt dieser Punkt):** kein Bloom, Kanten 2 px in reinem Weiß auf Schwarz, Tönungen der Rähmchen durch Muster ersetzt (Schraffur-Textur je Art), Etiketten 16 px. - **`forced-colors`:** Die Karte zeigt den SVG-Rückfall mit `CanvasText`-Strichen. ### 9.4 Bedienung in der Karte | Aktion | Maus | Touch | Ergebnis | |---|---|---|---| | Teil berühren | Hover | erstes Tippen | Kanten leuchten stärker, Teil hebt sich 8 mm, Etikett mit Namen. Andere Teile bleiben. | | Zarge öffnen | Klick | zweites Tippen | Fokus-Modus: Zarge fliegt nach vorn und wächst auf das 1,4-Fache, andere Teile weichen 300 mm zurück und verblassen auf 0,15. Rähmchen werden zu Platten. Knopf „Zurück“ erscheint. | | Rähmchen berühren (im Fokus) | Hover | erstes Tippen | Rähmchen steigt um 40 % seiner Höhe aus der Zarge, Etikett „R4 · Brutwabe · Name“. | | Rähmchen ändern | Klick | zweites Tippen | Sheet „Rähmchen 4“ (`components/ui/sheet.tsx`): Art als `ChoiceGroup` (`components/form/choice-group.tsx`), Name, „Speichern“. Speichert eine Änderung (Quelle `CARD`). | | Teil mit Zustand ändern | Klick auf Futterzarge, Boden, Holzdeckel, Zwischenboden, Verdunster | zweites Tippen | Sheet mit Zustandsfeldern (`NumberStepper` für Pegel, `ChoiceGroup` für `choice`), beim Boden zusätzlich „Bausperre einlegen/entfernen“, bei einer Zarge „Verdunster einsetzen/entfernen“. | | Drehen | Ziehen | Wischen mit einem Finger | Drehung um die Hochachse, ±60°, federt zurück auf die Grundansicht, wenn 4 s nichts passiert. Kein Zoom in der Karte. Senkrechtes Wischen scrollt die Seite. | | Fokus verlassen | Klick ins Leere, Esc | Tippen ins Leere, „Zurück“ | Alles fliegt in den Stapel zurück. | | Umbauen | Knopf „Umbauen“ | dito | Vollbild (9.5). | - **Ruhebewegung (nur `full`):** Das Modell schwebt kaum merklich (±4 mm, 6 s) und folgt dem Mauszeiger mit einer Neigung von höchstens 6°. Keine Ruhebewegung bei `subtle`/`off`, wenn die Karte nicht sichtbar ist oder der Tab im Hintergrund liegt. - `frameloop="demand"`: Gezeichnet wird nur bei Bewegung oder Änderung. Ruhebewegung fordert Bilder an, solange sie läuft. - Die Karte zeigt unter dem Modell eine Zeile mit den abgeleiteten Werten: „2 Zargen · 6 Brutwaben · Futter 5 l · Bausperre drin“ und „Stand: 12.10. aus Durchsicht“. - Leser ohne `colonies:update` sehen alles, aber keine Sheets zum Ändern und keinen Knopf „Umbauen“. - `NumberStepper` kann heute nur ganze Zahlen. L2 erweitert ihn um Dezimalschritte (`step` 0,5) und Eingabe mit deutschem Komma; bestehende Aufrufe bleiben ganzzahlig. ### 9.5 Vollbild „Umbauen“ - Eigene Route `/colonies/[id]/hive` (Zurück-Pfad zur Volk-Seite, kein Modal, damit „Zurück“ im Browser funktioniert). **Nur online**: Offline zeigt der Knopf „Umbauen“ den Hinweis „Umbauen geht nur mit Netz“; kleine Änderungen in der Karte gehen offline weiter (Abschnitt 10). - Links bzw. unten (Handy: Bottom-Sheet) die **Teile-Leiste**: aktive Teile-Arten des Systems, gruppiert (Zargen, Gitter und Böden, Fütterung und Behandlung, Abdeckung und Deckel). Jede Zeile hat ein kleines SVG-Symbol aus `hiveLayout`. - **Drag & Drop in 3D:** Beim Ziehen eines Teils öffnet sich der Stapel leicht (Abstand 60 mm zwischen den Teilen) und an jeder erlaubten Stelle leuchtet eine Einfügefläche. Über einer Fläche schnappt eine durchsichtige Vorschau ein. Loslassen fügt ein. Einlagen und Einsätze schnappen nur auf passende Wirte (Boden, Zarge). Zeiger-Ereignisse (Pointer Events), damit Maus, Stift und Finger gleich funktionieren. - **Ohne Ziehen:** Jede Zeile der Teile-Leiste hat „Hinzufügen …“ mit Auswahl der Stelle („über Zarge 1“). Das ist auch der Weg für Tastatur und Screenreader. - **Verschieben und Entfernen:** Teile im Stapel per Ziehen verschieben; zum Entfernen auf die Fläche „Entfernen“ ziehen oder im Sheet „Entfernen“. In der Aufbau-Liste mit „Nach oben“/„Nach unten“/„Entfernen“. - Neue Zargen starten mit allen Plätzen als `FOUNDATION`. - Änderungen sammeln sich lokal (mit Rückgängig/Wiederholen, Strg+Z/Strg+Umschalt+Z). „Speichern“ schickt sie als **eine** Änderung (Quelle `EDITOR`) mit Datum (Standard: heute, Berliner Tag). „Verwerfen“ fragt nach, wenn etwas offen ist. Verlassen der Seite mit offenen Änderungen fragt ebenfalls. - Zoom (Mausrad, zwei Finger) ist hier erlaubt, begrenzt auf 0,6–2,5. ### 9.6 Aufbau-Liste (Tastatur und Screenreader) - Neben dem Canvas liegt immer eine Liste als Baum (`role="tree"`), von oben nach unten so, wie man die Beute öffnet: Dach, Deckel, …, Boden; je Zarge die Rähmchen als Unterpunkte. In der Karte ist sie optisch versteckt, aber erreichbar; der sichtbare Knopf „Aufbau als Liste“ blendet sie ein. Im Vollbild ist sie sichtbar. - Der Canvas ist `aria-hidden`. Fokus in der Liste hebt das Teil in 3D hervor (wie Hover); Enter auf einer Zarge öffnet den Fokus-Modus; auf einem Rähmchen oder Teil mit Zustand das Sheet. - Jede Änderung meldet eine Live-Region: „Rähmchen 4 in Zarge 1 ist jetzt Brutwabe.“ - Tab-Reihenfolge: Knopf „Umbauen“, „Aufbau als Liste“, Liste, „Zurück“ (im Fokus-Modus). Fokusstil aus T5. ### 9.7 Rückfall ohne WebGL - Vor dem Laden des 3D-Bundles zeigt die Karte das **SVG** aus `hiveLayout` (Schrägansicht, gleiche Farben). Ist der 3D-Teil geladen und WebGL verfügbar, blendet er über. - Kein WebGL, Kontextverlust ohne Wiederherstellung, Bewegungsstufe `off` mit schwachem Gerät (`navigator.hardwareConcurrency <= 2`) oder `forced-colors`: Die Karte bleibt beim SVG. Bedienung über SVG-Klicks und die Liste, gleiche Sheets. - Druck: SVG. ### 9.8 Bewegung | Stufe | Flüge, Heben, Pegel-Wogen, Ruhebewegung | Zustandswechsel | |---|---|---| | `full` (und kein `prefers-reduced-motion`) | an, mit Federn (`maath/easing` `damp3`, ca. 350 ms) | animiert | | `subtle` | aus | Überblendung 150 ms (Deckkraft, Farbe); Teile springen an die Zielstelle | | `off` | aus | sofort | Die Stufe kommt aus einem neuen Hook `useEffectiveMotionLevel()` in `lib/motion`: er nutzt `effectiveMotionLevel()` und hört auf `matchMedia("(prefers-reduced-motion: reduce)")` und auf Änderungen der gespeicherten Stufe, damit Szene und DOM sofort reagieren. Die DOM-Teile (Sheets, Liste, Teile-Leiste, Etiketten-Wechsel) nutzen `motion` (`motion/react`) mit diesem Hook. ### 9.9 Leistung - Das 3D-Bundle (three, fiber, drei-Teile, postprocessing) wird nur per `next/dynamic` mit `ssr: false` geladen, wenn die Karte in den Sichtbereich kommt. Budget: höchstens 320 kB gzip für diesen Chunk, gemessen im Build. Andere Seiten laden nichts davon. - Benannte drei-Importe mit Tree-Shaking, keine Sammelimporte. - Geometrien und Materialien werden je Teile-Art geteilt; Rähmchen als `InstancedMesh` je Zarge. - `dpr={[1, 2]}`, Bloom mit halber Auflösung. Ziel 60 fps während eines Flugs auf einem Mittelklasse-Handy, Mindestmaß 30 fps (Performance-Trace bei 4-facher CPU-Drosselung). - Der Canvas wird beim Verlassen der Seite freigegeben (`dispose`), kein Speicherleck beim Wechsel zwischen Völkern (Heap-Snapshot im Browser-Test). ### 9.10 Historie - In der Karte: „Verlauf“ öffnet eine Liste der Änderungen (Tag, Quelle, Kurztext wie „Bausperre entfernt, Zarge 2 hinzugefügt“). Der Kurztext wird aus den Operationen erzeugt (`describeHiveOps`, Frontend, reine Funktion). - Antippen einer Zeile zeigt den Stand danach im selben Modell, mit Leiste „Stand vom 12.08. · Zurück zu heute“. Bearbeiten ist dort aus. - Die Lebenslinie aus T5 kann die Änderungen später als Ereignisse zeigen; das ist nicht Teil dieser Spec. ## 10. Offline - **Lesen:** Das Stand-Paket (`OfflinePackV1`) bekommt die Aufbauten der Völker des Stands und den Katalog als **optionale** Felder (wie `nextSteps`), Backend- und Frontend-Typ gleich. `GET /colonies/:id/hive` kommt in `NEVER_CACHED_API_PATHS` (`/\/api\/v1\/colonies\/[^/]+\/hive\/?$/i`, `SW_ROUTES_VERSION` erhöhen) und wird über `withPackFallback` gelesen; sonst verdeckt eine alte zwischengespeicherte Antwort das neuere Paket. `GET /hive-systems` bleibt normal zwischengespeichert, mit dem Paket als Rückfall. - Die Offline-Volkseite `/offline/colony` zeigt die Beute schreibgeschützt als SVG mit Aufbau-Liste aus dem Paket. - **Schreiben:** Änderungen aus der Karte gehen offline in eine neue Warteschlange `pending-hive-changes` (Offline-DB Version 5, Eintrag in `RecordQueueInputs` und den Warteschlangen-Texten, mit `Idempotency-Key`, Besitzer, Fehleranzeige im Warteschlangen-Sheet wie die anderen). - **Reihenfolge:** Heute leert `runSync` alle Warteschlangen parallel. Neu ist ein Drain `drainHiveTimeline`: Er liest `pending-hive-changes` und die Einträge aus `pending-inspections` mit `hiveChange`, sortiert sie nach `queuedAt` und sendet sie nacheinander. Der normale Durchsicht-Drain überspringt Einträge mit `hiveChange`. Scheitert ein Eintrag nicht nur vorübergehend (Antwort ≠ Netzfehler/5xx), hält der Drain spätere Beute-Einträge desselben Volks zurück, bis der Nutzer den Fehler auflöst. - **Anzeige:** Die Karte zeigt den Stand mit allen wartenden Änderungen angewandt, aus beiden Quellen (`pending-hive-changes` und `hiveChange` wartender Durchsichten), und einen Hinweis „1 Änderung wartet auf Netz“. ## 11. Fehlerfälle | Fall | Verhalten | |---|---| | 409 beim Speichern online | Sheet „Die Beute wurde inzwischen geändert“ zeigt den aktuellen Stand; die eigenen Operationen werden auf ihn neu angewandt und als Vorschau gezeigt. „Erneut speichern“ oder „Verwerfen“. Gelingt das Neu-Anwenden nicht, nennt das Sheet die Operation und den Grund. | | 409 `HIVE_CONFLICT` in der Warteschlange | wie Abschnitt 7 bzw. 10, Knöpfe „Verwerfen“ und bei Durchsichten „Ohne Beute-Änderung senden“. Andere 409 (Idempotenz) behandelt die Warteschlange wie bisher. | | 403 bei Durchsicht mit Beute-Änderung | Warteschlange zeigt „Keine Berechtigung, die Beute zu ändern“ mit „Ohne Beute-Änderung senden“. | | Teile-Art ausgeblendet | Bestehende Teile bleiben und zeigen „(ausgeblendet)“; die Teile-Leiste bietet sie nicht mehr an. | | Unbekannte Teile-Art im JSON | Darf nicht vorkommen (Validator). Falls doch (manuelle DB-Änderung): Teil als grauer Kasten mit „Unbekanntes Teil“, Fehler im Server-Log. | | WebGL-Kontext verloren | Einmal neu aufbauen; beim zweiten Verlust SVG (9.7). | | Volk ohne Aufbau | Karte „Noch keine Beute angelegt“ mit Vorschau der Vorlage und „Übernehmen“ / „Wie anderes Volk“. Leser ohne `colonies:update` sehen nur den Text. | | Volk lebt nicht mehr (aufgelöst, eingegangen, …) | Modell mit letztem Stand, keine Bearbeiten-Knöpfe. | | Laden fehlgeschlagen | Fehlermeldung in der Karte mit „Erneut versuchen“, Höhe bleibt (kein Springen). | ## 12. Tests und Prüfung ### 12.1 Backend - Unit: `applyHiveOps` (jede Operation, jede Regel aus 4.1, Grenzen der Zustandsfelder, 100-Operationen- und 30-Teile-Grenze, Begrenzung von `at`), `deriveHiveFacts`, Validator. Fixtures in `test/fixtures/hive-ops/`. - Unit Service: Sperren (Colony, dann HiveSetup) und Version, 400 gegen 409 `HIVE_CONFLICT`, `occurredOn`-Regeln (Berliner Tag, nicht vor `INIT`), „Stand am Tag“ mit Nachtrag und vor `INIT`, zweites Anlegen 409 `HIVE_EXISTS`, Outbox-Ereignis `HiveChanged` in derselben Transaktion, Idempotenz. - e2e: Katalog lesen (`colonies:read`) und mit `hive-systems:manage` ändern (ohne: 403), Vorlage, Aufbau anlegen, Änderung, Historie mit Cursor, Stand am Tag, Durchsicht mit Beute-Änderung (gemeinsam gespeichert, gemeinsam abgelehnt; STAFF ohne `colonies:update` 403), nicht lebendes Volk schreibgeschützt, Backup-Export und -Import mit den neuen Modellen (auch in eine frische Installation mit Startdaten). ### 12.2 Frontend - Unit: `hiveLayout` (Höhen, Gesamt-Box, Rähmchen-Lagen, Einlagen, Einsätze), Frontend-`applyHiveOps` gegen dieselben Fixtures, `hiveFollowUps`, `describeHiveOps`. - Komponenten (Vitest + RTL): Aufbau-Liste (Tastatur, Live-Region, Sheets), SVG-Rückfall, Karte ohne Aufbau, 409-Ablauf, Durchsicht-Abschnitt „Beute“ mit Rückgängig, `NumberStepper` mit Dezimalschritten, `useEffectiveMotionLevel`, Volk-Seite mit neuem Raster, Offline-Volkseite mit Beute. - Offline: `drainHiveTimeline` (Reihenfolge über beide Warteschlangen, Zurückhalten nach Fehler, Durchsicht-Drain überspringt), DB-Migration auf Version 5, `sw-routes` mit dem neuen Pfad. - Szene: `@react-three/test-renderer` für Hover, Klick und Fokus-Modus (Zielwerte aus `use-hive-motion`), ohne echtes WebGL. - Playwright: Volk öffnen, Zarge über die Liste öffnen, Rähmchen-Art ändern, Seite neu laden, Änderung da; Umbauen mit „Hinzufügen …“ (ohne Ziehen) und Speichern. - Browser-Prüfung mit chrome-devtools: Desktop 1280 und 390 px, gleiche Höhe wie Stammdaten, 1 und 6 Zargen ohne Abschnitt, hell/dunkel, `prefers-reduced-motion`, Tastatur, Performance-Trace eines Flugs, Heap-Snapshot nach zehn Volk-Wechseln. ### 12.3 Abhängigkeiten Neu im Frontend: `three`, `@react-three/fiber`, `@react-three/drei`, `@react-three/postprocessing`, `postprocessing`, `motion`; dev: `@types/three`, `@react-three/test-renderer`. Versionen exakt gepinnt, jede mindestens 14 Tage alt, Peer-Bereiche passend zu React 19.2 (fiber 9 verlangt `react <19.4`, postprocessing verlangt `three <0.187`). Lizenzen MIT bzw. Zlib. Vor dem Einbau Prüfung mit dem Skill `supply-chain-risk-auditor`. ### 12.4 Folgen für T5 T5 §1 schreibt „Keine neue Animation“ und „keine neue Laufzeit-Bibliothek“. Der Inhaber will seit 2026-10-15 ausdrücklich Bewegung und eine hochwertigere Optik. Diese Spec führt `motion` ein und begrenzt sich auf die Beute. Vor der Umsetzung von T5 wird die T5-Spec um eine Bewegungs-Sprache ergänzt (eigene Rückfrage an den Inhaber), statt die Regeln hier stillschweigend auf die ganze App auszudehnen. Eintrag in TASKS.md. ## 13. Lieferungen Jede Lieferung ist für sich lauffähig und wird einzeln ausgerollt (nach Freigabe des Inhabers). 1. **L1 Daten und API:** Prisma-Modelle und Migration, Katalog mit Startdaten und festen Ids (Abschnitt 4.3) per Migration, Berechtigung `hive-systems:manage`, `applyHiveOps`, Validator, Facts, Endpunkte 6.2, Durchsicht mit `hiveChange` (Backend), Ereignis `HiveChanged`, Backup. Keine Oberfläche. 2. **L2 Karte ohne 3D:** Typen, Frontend-Operationen, `hiveLayout`, SVG, Aufbau-Liste, `NumberStepper` mit Dezimalschritten, Sheets für Rähmchen und Zustand, Karte auf der Volk-Seite (neues Raster, gleiche Höhe), „Aufbau anlegen“, Historie, Vorschläge Fütterung und Behandlung. Damit ist die Funktion online vollständig nutzbar, nur ohne 3D. 3. **L3 3D-Szene:** Abhängigkeiten, Canvas, Stil, Einpassen, Hover, Fokus-Modus, Rähmchen-Heben, Pegel, Bewegungsstufen, Leistung. 4. **L4 Umbauen:** Vollbild-Route, Teile-Leiste, Drag & Drop in 3D, „Hinzufügen …“, Rückgängig, Speichern. 5. **L5 Durchsicht und Offline:** Abschnitt „Beute“ im Formular, Stand-Paket mit Aufbauten, Lesen über `withPackFallback`, Offline-Volkseite, Warteschlange `pending-hive-changes`, `drainHiveTimeline`. ## 14. Annahmen und offene Punkte | Nr. | Annahme | Prüfung | |---|---|---| | A1 | Außenmaß der Liebig-Zander-Zarge 525 × 430 mm (aus Innenmaß Edelstahldeckel). | Inhaber misst oder schickt Fotos; Katalogwert ändern. | | A2 | Styropor-Einlage 465 × 365 × 20 mm. | Inhaber misst. | | A3 | Nassenheider Professional liegt in einer Leerzarge auf den Rähmchen der Zarge darunter; im Modell ein Einsatz der Leerzarge. | Inhaber bestätigt mit Foto. | | A4 | Rähmchen-Plätze zählen von links, mit Blick auf das Flugloch. | Inhaber bestätigt. | | A5 | Aufbau ändern mit `colonies:update`, Katalog mit `hive-systems:manage` (ADMIN). STAFF kann die Beute damit nicht ändern, auch nicht in der Durchsicht. | Inhaber entscheidet, ob STAFF `colonies:update` bekommt (Rollenverwaltung, kein Code). | | A6 | Die Bausperre ist eine Einlage ohne Zustand; „Winter“ ist kein eigener Modus, sondern Einlegen und Entfernen. | Inhaber. | Diese Annahmen kommen nach ARCHITECTURE.md „Annahmen & offene Fragen“, sobald L1 gemergt ist.