Files
ApiCore/docs/superpowers/plans/2026-10-15-beute-3d.md
T
2026-10-15 20:00:33 +02:00

378 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Beute in 3D Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Each colony gets a hive setup (Ist-Zustand) with catalog, operations and history, shown as an animated holographic 3D model on the colony page, editable in the card, in a full-screen editor and through inspections, online and offline.
**Architecture:** Backend module `src/hive` stores the setup as validated JSON per colony plus an append-only change log; all writes are operations applied by one pure function under row locks. Frontend `lib/hive` mirrors the pure functions (tested against shared JSON fixtures) and `components/hive` renders one shared layout as SVG (fallback, offline, print) and as a lazily loaded react-three-fiber scene.
**Tech Stack:** NestJS 11, Prisma 7 (PostgreSQL), Jest; Next.js 16, React 19.2, TanStack Query 5, Vitest + RTL, three 0.186.1, @react-three/fiber 9.8.1, @react-three/drei 10.7.9, @react-three/postprocessing 3.1.3, postprocessing 6.39.5, motion 13.4.6.
**Spec:** `docs/superpowers/specs/2026-10-15-beute-3d-design.md`
## Global Constraints
- Work on `main`. Agents do not commit; the orchestrator commits once per delivery (L1…L5) after the gates.
- Gates per project: `npm run lint`, `npm run typecheck`, `npm test`, `npm run build`; backend also `npm run test:e2e` (test DB `localhost:5433/apicore_test`).
- Before any Next.js code, read the matching guide in `ApiCore-Frontend/node_modules/next/dist/docs/` (AGENTS.md).
- Permissions: reads `colonies:read`; setup writes `colonies:update`; catalog writes `hive-systems:manage` (new, system role ADMIN via catalog sync, no grant migration).
- Days are Berlin calendar days `YYYY-MM-DD` (`berlinToday`, `berlinDayOf` in `src/common/dates.ts`); `occurredOn` never in the future, never before the INIT day.
- Errors: 400 for shape errors; 409 `{ code: "HIVE_CONFLICT", opIndex, reason, current: { version, state } }` for ops failing against the current state; 409 `HIVE_EXISTS` for a second setup; 409 like `assertActive` for non-living colonies.
- Limits: ≤ 100 ops per request, ≤ 30 parts in the stack, labels and frame names ≤ 40 chars.
- Lock order: `Colony` FOR UPDATE, then `HiveSetup` FOR UPDATE (`HiveSetup` directly after `Colony` in `LOCKABLE_TABLES`).
- Hive facts never write into `Colony` or `Inspection`. `Colony.occupiedFrames` stays colony strength.
- Feeding and treatment are only ever suggested (prefilled form), never booked automatically.
- UI copy German, short sentences, state never by colour alone (icon or word too), touch targets ≥ 44 px, visible focus.
- Motion: levels `full`/`subtle`/`off` plus `prefers-reduced-motion` via `useEffectiveMotionLevel()`; `subtle` = 150 ms fades, no spatial movement; `off` = instant.
- 3D chunk ≤ 320 kB gzip, loaded only via `next/dynamic` (`ssr: false`) when the card is visible; no 3D code on other pages.
- New dependencies pinned exactly to the versions in Tech Stack (each ≥ 14 days old on 2026-10-15); `supply-chain-risk-auditor` skill before installing.
- No Playwright exists in this repo: browser checks use the chrome-devtools MCP (they replace the Playwright line of spec §12.2, see Tasks 12, 16, 18, 22).
## Review Focus
1. **Offline edit, then someone else removes the part:** queued `setFrame` on a removed box → 409 HIVE_CONFLICT, queue shows the entry with „Verwerfen“, later entries of that colony wait. Test in Task 21.
2. **Decimal levels with German comma:** „2,5“ kg in the stepper must store 2.5; „2,25“ with step 0.5 must be rejected client- and server-side. Tests in Tasks 2 and 9.
3. **Tall Stammdaten card:** conflict/restriction hints make Stammdaten taller; the hive card must grow with it and re-fit without cropping. Test in Task 12 (grid classes) and browser check in Task 16.
4. **Colony without setup opened offline:** `/offline/colony` and the card must show „Noch keine Beute angelegt“ read-only, not crash. Test in Task 20.
5. **Backup restore into a fresh install:** the seeded catalog with fixed ids must not duplicate; restore must succeed. Test in Task 6.
---
## L1 — Daten und API (Backend)
### Task 1: Schema, migration, seed catalog, permission
**Files:**
- Modify: `ApiCore-Backend/prisma/schema.prisma` (models `HiveSystem`, `HivePartType`, `HiveSetup`, `HiveChange`, enums `HivePartKind`, `HiveBoxDepth`, `HiveChangeSource`; back-relations `Colony.hiveSetup`, `Inspection.hiveChange`, `User.hiveChanges`) — exactly spec §5.1.
- Create: `ApiCore-Backend/prisma/migrations/20261017100000_hive_setup/migration.sql` (tables) and `20261017100100_hive_catalog_zander_liebig/migration.sql` (seed rows).
- Modify: `ApiCore-Backend/src/prisma/row-lock.ts` (`HiveSetup: lockable('HiveSetup', 'colonyId')` directly after `Colony`).
- Modify: `ApiCore-Backend/src/auth/permissions/permission-catalog.ts` (group „Beutensysteme“, key `hive-systems`, action `manage`, label „Pflegen“).
- Test: `ApiCore-Backend/test/hive-catalog-seed.e2e-spec.ts`, the existing permission-catalog spec.
**Interfaces:**
- Produces: Prisma models above. Fixed ids: system `hive-system-zander-liebig-10`; part types `hive-part-zl10-floor`, `-floor-insert`, `-box-10`, `-box-empty`, `-divider-board`, `-queen-excluder`, `-feeder`, `-cover-abdeckfix`, `-cover-lotus`, `-propolis-screen`, `-inner-lid`, `-insulation`, `-roof`, `-evaporator` (all prefixed `hive-part-zl10-`), dimensions from spec §4.3.
- State schemas: floor `entrance` (OPEN/NARROW/CLOSED, „offen/eng/zu“), `varroaTray` (IN/OUT, „drin/draußen“); divider `bungHoles` (OPEN/CLOSED/BEE_ESCAPE, „offen/zu/Bienenflucht“); inner lid `feedHole` (OPEN/CLOSED, „offen/zu“); feeder `syrupLiters` (l, max 3, step 0.5, feedType SUGAR_WATER) and `fondantKg` (kg, max 2.5, step 0.5, feedType FEED_PASTE); evaporator `acidMl` (ml, max 290, step 10). All others `[]`.
- System template (spec §4.4), ids omitted: floor, box-10 (slots 1–10 `FOUNDATION`), cover-abdeckfix, inner-lid, insulation, roof.
- [ ] **Step 1: Write failing e2e `hive-catalog-seed.e2e-spec.ts`**: system `hive-system-zander-liebig-10` exists with 14 active part types; `hive-part-zl10-box-10` has `frameSlots 10`, `heightMm 230`; feeder `stateSchema` has two `level` fields with `feedType`; `hive-systems:manage` is in `PERMISSION_CATALOG` and the ADMIN role has it after catalog sync.
- [ ] **Step 2: Run** `npm run test:e2e -- hive-catalog-seed` → FAIL (table missing).
- [ ] **Step 3: Implement** schema, both migrations (`npx prisma migrate dev --create-only`, seed INSERTs by hand), lock entry, permission.
- [ ] **Step 4: Run** the e2e and `npm test -- permission` → PASS; `npm run typecheck` clean.
### Task 2: Pure hive domain (ops, validator, facts) with shared fixtures
**Files:**
- Create: `ApiCore-Backend/src/hive/hive-types.ts`, `hive-ops.ts`, `hive-validate.ts`, `hive-facts.ts`
- Create: `ApiCore-Backend/test/fixtures/hive-ops/catalog.json` (Task 1 catalog as JSON array) and case files `*.json` (≥ 20: each op succeeding, each rule of spec §4.1, level step and max, unknown state key, 100-op limit, 30-part limit, `at` clamping, duplicate id, foreign and inactive `partTypeId`)
- Test: `ApiCore-Backend/src/hive/hive-ops.spec.ts`, `hive-facts.spec.ts`
**Interfaces:**
- Produces:
- Types `HiveState`, `HivePart`, `HiveFrame`, `FrameType`, `HiveOp` exactly as spec §5.2/§6.1.
- `HiveCatalog = { systemId: string; partTypes: Map<string, CatalogPartType> }`, `CatalogPartType = { id; kind: HivePartKind; name; lengthMm; widthMm; heightMm; frameSlots: number | null; stateSchema: StateField[]; active: boolean }`, `StateField = LevelField | ChoiceField` per spec §4.2; `catalogFromRows(rows: CatalogPartType[], systemId): HiveCatalog`.
- `applyHiveOps(state: HiveState, ops: HiveOp[], catalog: HiveCatalog): { ok: true; state: HiveState } | { ok: false; opIndex: number; reason: string }` — never mutates input; validates after every op.
- `validateHiveState(state: unknown, catalog: HiveCatalog): string[]` (empty = valid).
- `deriveHiveFacts(state, catalog): HiveFacts = { boxCount; broodFrames; honeyFrames; builtFrames; levels: { partId; key; label; unit; value; max; feedType?: string }[]; hasFloorInsert; evaporatorAcidMl: number | null }`.
- Reason strings start with a stable code: `MISSING_ID`, `RULE_FLOOR_BOTTOM`, `RULE_ROOF_TOP`, `RULE_INSERT_HOST`, `RULE_INSERT_LIMIT`, `STATE_UNKNOWN_KEY`, `STATE_OUT_OF_RANGE`, `STATE_STEP`, `FRAMES_LENGTH`, `LIMIT_PARTS`, `LIMIT_TEXT`, `DUPLICATE_ID`, `FOREIGN_PART_TYPE`, `INACTIVE_PART_TYPE` followed by `: detail`.
- Fixture format (shared with Task 8): `{ "name", "state", "ops", "expect": { "ok": true, "state" } | { "ok": false, "opIndex", "reasonCode" } }`; failures compare the code only.
- Rules beyond spec §4.1: existing parts with an inactive type stay valid, but `addPart`/`addInsert` with one fails `INACTIVE_PART_TYPE`; `at` outside `[0, stack.length]` is clamped; a level value must be a multiple of `step` (tolerance 1e-9).
- [ ] **Step 1: Write fixtures and `hive-ops.spec.ts`** looping over every case file: result matches `expect`; plus „does not mutate input“ with a deep-frozen state.
- [ ] **Step 2: Write `hive-facts.spec.ts`**: template → `boxCount 1, broodFrames 0, builtFrames 0, hasFloorInsert false`; 6 BROOD, 2 HONEY, 1 DIVIDER, 1 NONE → `broodFrames 6, honeyFrames 2, builtFrames 8`; feeder at 2.5 l → one level entry with `feedType "SUGAR_WATER"`.
- [ ] **Step 3: Run** `npm test -- src/hive` → FAIL.
- [ ] **Step 4: Implement** the four files.
- [ ] **Step 5: Run** → PASS.
### Task 3: Catalog API
**Files:**
- Create: `ApiCore-Backend/src/hive/hive.module.ts`, `hive-catalog.controller.ts`, `hive-catalog.service.ts`, `dto/create-hive-system.dto.ts`, `dto/update-hive-system.dto.ts`, `dto/create-hive-part-type.dto.ts`, `dto/update-hive-part-type.dto.ts`
- Modify: `ApiCore-Backend/src/app.module.ts` and `src/all-in-one.module.ts` where modules are listed
- Test: `ApiCore-Backend/src/hive/hive-catalog.service.spec.ts`
**Interfaces:**
- Consumes: Task 2.
- Produces:
- `HiveCatalogService.listSystems(): Promise<HiveSystemDto[]>` (`HiveSystemDto = HiveSystem & { partTypes: HivePartType[] }`, active and inactive, ordered by name).
- `HiveCatalogService.loadCatalog(systemId: string, tx?: TxClient): Promise<HiveCatalog>`.
- `HiveCatalogService.template(systemId: string): Promise<HiveState>` (fresh `randomUUID()` per part).
- Routes of spec §6.2 with `@RequirePermission`; catalog writes recorded with `auditLog.record` like other master data.
- Part-type updates may only append `stateSchema` fields (otherwise 400 `SCHEMA_NOT_ADDITIVE`); a system `template` must pass `validateHiveState`.
- [ ] **Step 1: Write failing service spec**: template has new ids on each call and is valid; removing a state field → 400 `SCHEMA_NOT_ADDITIVE`; deactivating sets `active false`, never deletes.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 4: Setup service and API (create, changes, history, at)
**Files:**
- Create: `ApiCore-Backend/src/hive/hive.controller.ts`, `hive.service.ts`, `hive-errors.ts`, `dto/create-hive-setup.dto.ts`, `dto/create-hive-change.dto.ts`, `dto/hive-op.dto.ts`
- Modify: `ApiCore-Backend/src/domain-events/domain-event-types.ts` (`HiveChanged: { colonyId: string; version: number; source: HiveChangeSource; inspectionId: string | null }`, aggregate `Colony`), `src/domain-events/domain-events.service.spec.ts` (type list), `src/webhooks/webhook.handlers.spec.ts` (not forwarded)
- Test: `ApiCore-Backend/src/hive/hive.service.spec.ts`
**Interfaces:**
- Consumes: Tasks 2–3; `lockRowForUpdate` (`src/prisma/row-lock.ts`), `isLivingColonyStatus` (`src/colonies/colony-status.ts`), `berlinToday`, `events.record(tx, …)`.
- Produces:
- `HiveSetupView = { colonyId; systemId; version; state: HiveState; facts: HiveFacts; updatedAt: string }`.
- `HiveService.get(colonyId): Promise<HiveSetupView>`; 404 `HIVE_NOT_FOUND` without setup.
- `HiveService.create(colonyId, { systemId, state, occurredOn }, userId): Promise<HiveSetupView>` → version 1 plus `HiveChange` source INIT.
- `HiveService.applyChange(tx: TxClient, colonyId, input: { occurredOn: string; source: HiveChangeSource; ops: HiveOp[]; note?: string; inspectionId?: string }, userId): Promise<HiveSetupView>` — locks Colony then HiveSetup; checks living status and `occurredOn`; applies ops; failure throws `HiveConflictException` (409 body of Global Constraints); writes `HiveChange` (version + 1, `stateAfter`), updates the setup, records `HiveChanged`. Used by the controller and by Task 5.
- `HiveService.history(colonyId, cursor?: string, limit = 20): Promise<{ items: HiveChangeListItem[]; nextCursor: string | null }>`, `HiveChangeListItem = { id; version; occurredOn; source; inspectionId; userId; userName; ops; note; createdAt }`, ordered `occurredOn desc, version desc`.
- `HiveService.at(colonyId, day: string): Promise<{ version; state; facts; occurredOn }>` per spec §6.2 (404 before INIT).
- Controller: `POST /colonies/:id/hive` and `POST /colonies/:id/hive/changes` with `@Idempotent()`; DTO `source` accepts only `CARD | EDITOR` (`AI_SUGGESTION` → 400); `ops` `@IsArray @ArrayMaxSize(100) @ValidateNested`.
- [ ] **Step 1: Write failing service spec**: create twice → 409 `HIVE_EXISTS`; change on a non-living colony → 409; missing id → 409 HIVE_CONFLICT with `current`; `occurredOn` tomorrow (Berlin) → 400; before INIT → 400; version increments; `HiveChanged` written in the same transaction (a rolled-back transaction leaves no event); `at` returns the highest version with `occurredOn <= day` and 404 before INIT; a backdated change sorts by `occurredOn` in history.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** `npm test -- src/hive src/domain-events src/webhooks` → PASS.
### Task 5: Inspection with `hiveChange` (backend)
**Files:**
- Modify: `ApiCore-Backend/src/inspections/dto/create-inspection.dto.ts` (`hiveChange?: { ops: HiveOpDto[] }`), `src/inspections/inspections.service.ts:61-107` (`create`), `src/inspections/inspections.module.ts` (import `HiveModule`)
- Test: `ApiCore-Backend/src/inspections/inspections.service.spec.ts`
**Interfaces:**
- Consumes: Task 4 `HiveService.applyChange`.
- Produces: `create()` destructures `hiveChange` together with `applyToColony`; checks `colonies:update` before the transaction when `hiveChange` is present (403 `HIVE_FORBIDDEN` otherwise); locks Colony before the inspection insert when `hiveChange` or `applyToColony` is present; calls `applyChange` with `source INSPECTION`, `occurredOn = berlinDayOf(new Date(dto.date))`, `inspectionId`; response adds `hive?: HiveSetupView`.
- [ ] **Step 1: Write failing tests**: inspection and ops saved together (one `HiveChange` with `inspectionId`); a conflicting op leaves no inspection row; a user without `colonies:update` gets 403 and no row; without `hiveChange` the existing specs stay green.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** `npm test -- src/inspections` → PASS.
### Task 6: Backup export and import
**Files:**
- Modify: `ApiCore-Backend/src/backup/backup.service.ts` (export around `:302-405`, import around `:545-693`), its coverage test
- Test: `ApiCore-Backend/src/backup/backup.service.spec.ts`, the existing backup e2e spec
**Interfaces:**
- Produces: export keys `hiveSystem`, `hivePartType`, `hiveSetup`, `hiveChange`; import order hiveSystem → hivePartType (upsert by id) → after colony: per imported colony delete its existing `HiveChange` and `HiveSetup`, then insert → after inspection: `hiveChange` with `userId: resolveUserId(...)`. Sealed backups only (not in `LEGACY_UNSEALED_TABLES`).
- [ ] **Step 1: Write failing tests**: the coverage test lists the four models; e2e export → reset DB (migrations incl. seed) → import succeeds, setup and history equal, catalog not duplicated.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** `npm test -- backup` and `npm run test:e2e -- backup` → PASS.
### Task 7: L1 e2e and docs
**Files:**
- Create: `ApiCore-Backend/test/hive.e2e-spec.ts`
- Modify: `ARCHITECTURE.md` (section „Beute“: module, storage form, lock order, permissions; spec §14 A1–A6 as Annahmen 95–100), `TASKS.md` (tick L1)
- [ ] **Step 1: Write e2e** for the spec §12.1 list: catalog read with `colonies:read`; catalog write without `hive-systems:manage` 403, with it 201; template; create; change; idempotent replay returns the same body; history cursor; `at`; inspection with `hiveChange` (STAFF 403); non-living colony 409.
- [ ] **Step 2: Run** `npm run test:e2e -- hive` → PASS (fix code, not tests).
- [ ] **Step 3: Gates** green. Orchestrator commits `feat: hive setup data model and API (Beute L1)`.
## L2 — Karte ohne 3D (Frontend)
### Task 8: `lib/hive` pure functions and API client
**Files:**
- Create: `ApiCore-Frontend/src/lib/hive/types.ts`, `ops.ts`, `layout.ts`, `facts.ts`, `describe.ts`, `follow-ups.ts`, `labels.ts`, `api.ts`
- Test: `ops.spec.ts` (runs the backend fixtures with `it.skipIf(!existsSync(dir))` like `lib/label-templates/placeholder-keys.spec.ts`), `layout.spec.ts`, `describe.spec.ts`, `follow-ups.spec.ts`
**Interfaces:**
- Consumes: JSON shapes of Tasks 2–4.
- Produces:
- Types mirroring Tasks 2/4 (`HiveState`, `HiveOp`, `HiveCatalog`, `HiveSetupView`, `HiveChangeListItem`, `HiveFacts`, `HiveSystemDto`).
- `applyHiveOps`, `deriveHiveFacts`, `catalogFromSystem(system: HiveSystemDto): HiveCatalog` with the same semantics and reason codes as the backend.
- `hiveLayout(state, catalog): HiveLayout`, `HiveLayout = { parts: PartBox[]; inserts: PartBox[]; frames: FrameBox[]; bounds: { w: number; h: number; d: number } }`, `PartBox = { id; partTypeId; kind; center: [number, number, number]; size: [number, number, number]; stackIndex: number }`, `FrameBox = { boxId; slot; type: FrameType; name?: string; center; size }`. 1 unit = 100 mm; stack grows on +y from 0; the roof overlaps the top 35 mm of the part below.
- `describeHiveOps(ops, before: HiveState, catalog): string` (e.g. „Bausperre entfernt, Zarge hinzugefügt“).
- `hiveFollowUps(ops, before, after, catalog): FollowUp[]`, `FollowUp = { kind: "feeding"; amount: number; unit: "l" | "kg"; feedType: FeedType } | { kind: "treatment"; amountMl: number } | { kind: "endTreatment" }` (spec §8).
- `labels.ts`: `FRAME_TYPE_LABELS: Record<FrameType, string>` (Brutwabe, Drohnenwabe, Honigwabe, Pollenwabe, Futterwabe, Mittelwand, Leerwabe, Baurahmen, Schied, leer), `PART_KIND_LABELS`.
- `api.ts`: `fetchHiveSystems()`, `fetchHive(colonyId)`, `fetchHiveTemplate(systemId)`, `createHive(colonyId, input, idempotencyKey)`, `postHiveChange(colonyId, input, idempotencyKey)`, `fetchHiveHistory(colonyId, cursor?)`, `fetchHiveAt(colonyId, day)`; query keys `["colonies", colonyId, "hive"]`, `["colonies", colonyId, "hive", "history"]`, `["hive-systems"]`.
- [ ] **Step 1: Write failing specs**: fixtures pass; `hiveLayout(template).bounds.h` = 4.31 ((120 + 230 + 1 + 35 + 20 + 60 − 35) / 100); 10 frames evenly spaced inside the box; feeder 0 → 3 l yields `{ kind: "feeding", amount: 3, unit: "l", feedType: "SUGAR_WATER" }`; evaporator added with 250 ml yields `{ kind: "treatment", amountMl: 250 }`; describe texts for addPart, removePart, setFrame.
- [ ] **Step 2: Run** `npx vitest run src/lib/hive` → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 9: Decimal `NumberStepper` and `useEffectiveMotionLevel`
**Files:**
- Modify: `ApiCore-Frontend/src/components/form/number-stepper.tsx` (optional prop `decimals?: number`, default 0; with `decimals > 0` accept `,` and `.`, display German comma, values on the `step` grid)
- Modify: `ApiCore-Frontend/src/lib/motion/motion-preference.ts` (`useEffectiveMotionLevel(): MotionLevel`, listening to the reduce-motion media query `change`, `storage` events and an in-app `apicore:motion` event dispatched by `setLevel`)
- Test: `number-stepper.spec.tsx`, `motion-preference.spec.ts`
- [ ] **Step 1: Failing tests**: with `decimals={1} step={0.5}` typing „2,5“ calls `onChange("2.5")`; „+“ from „2,5“ shows „3“; „2,25“ shows the field error „Nur Schritte von 0,5.“; without `decimals` the existing specs stay green. Hook returns `subtle` when reduce-motion matches and the stored level is `full`, and updates after the media `change` event.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 10: View store, SVG, outline tree
**Files:**
- Create: `ApiCore-Frontend/src/components/hive/use-hive-view.ts`, `hive-svg.tsx`, `hive-outline.tsx`
- Test: `use-hive-view.spec.ts`, `hive-svg.spec.tsx`, `hive-outline.spec.tsx`
**Interfaces:**
- Consumes: Task 8.
- Produces:
- `createHiveViewStore()`, `<HiveViewProvider store>`, `useHiveView(): { hoveredId: string | null; openBoxId: string | null; hoveredFrame: { boxId: string; slot: number } | null; setHovered(id: string | null): void; openBox(id: string | null): void; setHoveredFrame(frame | null): void }` (`useSyncExternalStore`).
- `<HiveSvg layout catalog onSelectPart?(id) onSelectFrame?(boxId, slot) />`: oblique projection (x + 0.5·z, y − 0.35·z), parts as outlined boxes with `data-part-id`, frames only for `openBoxId` with a pattern per type and the type word; strokes `currentColor` for `forced-colors`.
- `<HiveOutline state catalog canEdit announcement onEditPart(id) onEditFrame(boxId, slot) />`: `role="tree"`, roof first, boxes expandable, frames as `treeitem` „R4 · Brutwabe · Name“; arrows move, Enter opens the box or the edit sheet; focus calls `setHovered`; `announcement` goes to a polite live region.
- [ ] **Step 1: Failing tests**: outline order roof → floor; Down/Up/Right/Left/Enter behaviour; focus sets `hoveredId`; SVG has one `g[data-part-id]` per part and frames only for the open box; frame labels contain the type word.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 11: Edit sheets, mutation hook, follow-ups
**Files:**
- Create: `ApiCore-Frontend/src/components/hive/frame-sheet.tsx`, `part-sheet.tsx`, `use-hive-mutation.ts`, `follow-up-toast.ts`
- Modify: `ApiCore-Frontend/src/app/(app)/feedings/new/feeding-form-values.ts` and `src/app/(app)/treatments/new/treatment-form-values.ts` plus their pages (read optional search params `date`, `amount`, `unit`, `feedType` into the initial values, mapped onto the existing fields)
- Test: `frame-sheet.spec.tsx`, `part-sheet.spec.tsx`, `use-hive-mutation.spec.tsx`, both form-values specs
**Interfaces:**
- Consumes: Tasks 8–10, `ChoiceGroup` (`components/form/choice-group.tsx`), `Sheet` (`components/ui/sheet.tsx`), `NumberStepper` (`decimals`), the idempotency helper `lib/offline/idempotency.ts`.
- Produces:
- `useHiveMutation(colonyId): { submit(ops: HiveOp[], source: "CARD" | "EDITOR", occurredOn?: string): Promise<HiveSetupView>; conflict: HiveConflict | null; resolveConflict(action: "retry" | "discard"): Promise<void> }` — optimistic cache update; on 409 `HIVE_CONFLICT` sets `conflict = { current, preview: applyHiveOps(current.state, ops) }`; on success shows follow-up toasts with links `/feedings/new?colonyId=…&date=…&amount=…&unit=…&feedType=…` and `/treatments/new?colonyId=…&date=…&amount=…&unit=ml`.
- `<FrameSheet>`: ChoiceGroup of the 10 frame types, name input (≤ 40), „Speichern“ → one `setFrame`.
- `<PartSheet>`: one control per `stateSchema` field (stepper with `decimals` derived from `step`, ChoiceGroup for choice); floor also „Bausperre einlegen/entfernen“; box also „Verdunster einsetzen/entfernen“; „Speichern“ → ops.
- [ ] **Step 1: Failing tests**: FrameSheet saves `setFrame` with the chosen type and trimmed name; PartSheet feeder 2 → 5 l sends `setState` and shows „3 l Flüssigfutter nachgefüllt. Als Fütterung eintragen?“ with the prefilled link; 409 shows the conflict preview; the feeding form reads `amount=3&unit=l&feedType=SUGAR_WATER`.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 12: Hive card, init flow, history, colony page grid
**Files:**
- Create: `ApiCore-Frontend/src/components/hive/hive-card.tsx`, `hive-init.tsx`, `hive-history.tsx`, `hive-facts-line.tsx`
- Modify: `ApiCore-Frontend/src/app/(app)/colonies/[id]/page.tsx:91-110` (row 1 `lg:grid-cols-3` without `items-start`: `ColonyEditCard` + `HiveCard` with `lg:col-span-2`; row 2 with `items-start`: actions/QR + the cards grid)
- Test: `hive-card.spec.tsx`, `hive-init.spec.tsx`, `hive-history.spec.tsx`, `colonies/[id]/page.spec.tsx`
**Interfaces:**
- Consumes: Tasks 8–11, `useAuth().hasPermission`.
- Produces:
- `<HiveCard colonyId mode: "view" | "inspection" onOpsChange?(ops: HiveOp[]) />`: loads `fetchHive`; 404 → `<HiveInit>` (template preview, „Übernehmen“, „Wie anderes Volk“ with colony select; controls only with `colonies:update`); otherwise `HiveViewProvider` + visual slot (`<HiveSvg>` now, `HiveVisual` from Task 13) + `HiveOutline` (visually hidden until „Aufbau als Liste“) + facts line + „Umbauen“ (link `/colonies/[id]/hive`, hidden without permission, offline: hint „Umbauen geht nur mit Netz“) + „Verlauf“. `mode="inspection"` collects ops locally and reports them through `onOpsChange` instead of saving (Task 19). Card body `flex h-full min-h-0 flex-col`; below `lg` height `min(55svh,520px)`, minimum 320 px.
- `<HiveHistory colonyId onShowVersion(state: HiveState | null) />`: paged list with `describeHiveOps` text, „nachgetragen am …“ when `occurredOn` is before the `createdAt` day, follow-up buttons from `hiveFollowUps`; a selected row shows that state read-only with „Stand vom 12.08. · Zurück zu heute“.
- [ ] **Step 1: Failing tests**: the page renders Stammdaten and Beute in one grid row without `items-start`; without setup the card shows „Noch keine Beute angelegt“ and „Übernehmen“ only with permission; facts line „2 Zargen · 6 Brutwaben · Futter 5 l · Bausperre drin“; a history row shows the read-only state; offline shows „Umbauen geht nur mit Netz“.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** full `npm test`, lint, typecheck, build → PASS.
### Task 12b: Settings page „Beutensysteme“
**Files:**
- Create: `ApiCore-Frontend/src/app/(app)/settings/hive-systems/page.tsx`, `part-type-dialog.tsx`, `src/lib/hive/catalog-api.ts`
- Modify: the settings navigation entry list (`nav-config.ts` settings section), visible only with `hive-systems:manage`
- Test: `settings/hive-systems/page.spec.tsx`, `part-type-dialog.spec.tsx`
**Interfaces:**
- Consumes: Task 3 routes, Task 8 types.
- Produces: list of systems with their part types (name, kind, L × B × H mm, Plätze, Zustandsfelder, aktiv); dialog to add or edit a part type (name, article ref, dimensions, `frameSlots` for boxes, append `level`/`choice` fields, existing fields read-only, „Ausblenden“ instead of delete); system dialog for name, frame and footprint dimensions; template editing happens in the editor (Task 17: „Als Vorlage speichern“).
- [ ] **Step 1: Failing tests**: page hidden without permission (nav entry missing, page shows the no-access state); edit dimensions sends PATCH; existing state fields cannot be removed; „Ausblenden“ sends `active: false`.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** full `npm test`, lint, typecheck, build → PASS.
- [ ] **Step 5: Browser check** (chrome-devtools MCP, dev server with local backend): colony page at 1280 px and 390 px, card as tall as Stammdaten, keyboard through the outline, settings page. Orchestrator commits `feat: hive card with SVG, outline, history and catalog settings (Beute L2)`.
## L3 — 3D-Szene
### Task 13: Dependencies, tokens, lazy 3D slot with fallback
**Files:**
- Modify: `ApiCore-Frontend/package.json` (exact versions from Tech Stack; dev `@types/[email protected]`, `@react-three/[email protected]`), `src/app/globals.css` (`--holo-bg`, `--holo-edge`, `--holo-edge-strong`, `--holo-fill`, `--holo-label-bg`, `--holo-label-fg` in `:root` and `.dark`)
- Create: `ApiCore-Frontend/src/components/hive/hive-visual.tsx`, `src/lib/hive/webgl.ts`
- Test: `webgl.spec.ts`, `hive-visual.spec.tsx`
**Interfaces:**
- Produces: `canUseWebGL(): boolean`; `shouldUse3D({ webgl: boolean; motion: MotionLevel; cores: number; forcedColors: boolean }): boolean` (false without WebGL, with forced colors, or with `off` and ≤ 2 cores); `<HiveVisual layout catalog onSelectPart onSelectFrame />` renders `<HiveSvg>` at once and, when the card enters the viewport and `shouldUse3D` holds, loads `HiveCanvas` via `next/dynamic(() => import("./hive-scene/hive-canvas"), { ssr: false })` and cross-fades (motion-level aware); a second WebGL context loss falls back to SVG for good.
- [ ] **Step 1:** Run the `supply-chain-risk-auditor` skill on the eight packages; note the result in `.superpowers/sdd/2026-10-15-beute-3d/progress.md`; install with `npm install --save-exact`.
- [ ] **Step 2: Failing tests**: `shouldUse3D` cases above; `HiveVisual` shows SVG first and never imports the scene when `shouldUse3D` is false (mocked `next/dynamic`).
- [ ] **Step 3: Run** → FAIL. **Step 4: Implement.** **Step 5: Run** → PASS; `npm run build` puts the scene in its own chunk.
### Task 14: Scene, holographic style, auto-fit, motion targets
**Files:**
- Create: `ApiCore-Frontend/src/components/hive/hive-scene/hive-canvas.tsx`, `stack-part.tsx`, `frame-plate.tsx`, `fill-volume.tsx`, `use-hive-motion.ts`, `theme.ts`
- Test: `use-hive-motion.spec.ts`, `hive-canvas.spec.tsx` (`@react-three/test-renderer`)
**Interfaces:**
- Consumes: Task 8 layout, Task 10 view store, Task 9 motion hook.
- Produces:
- `hiveMotionTargets(layout: HiveLayout, view: HiveViewState): Map<string, { position: [number, number, number]; scale: number; opacity: number; frameMode: "hint" | "plate" }>` — pure; hover lift 0.08; open box z + 1.5 and scale 1.4, all other parts z − 3 and opacity 0.15, its frames `plate`; hovered frame lift 0.4 × frame height. The caller animates toward targets only at `full`, otherwise sets them (with a 150 ms opacity fade at `subtle`).
- `<HiveCanvas>`: `frameloop="demand"`, `dpr={[1, 2]}`, camera elevation 20°, azimuth 30°; drei `<Bounds fit clip observe margin={1.2}>` re-fitting on layout or size change (damped only at `full`); faces `MeshBasicMaterial` opacity 0.06–0.12 additive; drei `<Edges>` coloured from `--holo-edge` (read once by `theme.ts`); `EffectComposer` + `Bloom` at half resolution with a high luminance threshold; floor grid; motion with `maath/easing.damp3`; `FillVolume` with `sin` vertex wobble at `full`; disposes geometries and materials on unmount.
- [ ] **Step 1: Failing tests**: targets for hover, open box and frame hover as above; test renderer: one group per part, frames as hints until `openBoxId` is set; pointer over a part calls `setHovered(id)`.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 15: Interaction in the scene
**Files:**
- Modify: `hive-scene/stack-part.tsx`, `frame-plate.tsx`, `hive-canvas.tsx`
- Create: `ApiCore-Frontend/src/components/hive/hive-scene/hive-labels.tsx`, `use-touch-select.ts`
- Test: `use-touch-select.spec.ts`, `hive-canvas.spec.tsx` (extend)
**Interfaces:**
- Produces: spec §9.4 table — mouse hover/click; touch first tap = hover, second tap = select; box → `openBox(id)`; frame → `onEditFrame(boxId, slot)`; feeder, floor, inner lid, divider, evaporator → `onEditPart(id)`; Esc or empty click → `openBox(null)`; horizontal drag rotates ±60° around y (`touch-action: pan-y` keeps vertical page scroll) and springs back after 4 s; idle float ±0.04 over 6 s and pointer tilt ≤ 6° only at `full` and while visible; `<HiveLabels>` shows at most 3 drei `<Html>` labels (frame „R4 · Brutwabe · Name“, part name, level „5 l / 3 l“).
- [ ] **Step 1: Failing tests**: `useTouchSelect` first tap hovers, second tap selects, tap elsewhere resets; Esc closes the open box; at `subtle` no idle frames are requested.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 16: Performance and browser check (L3)
- [ ] **Step 1:** `npm run build`; scene chunk gzip ≤ 320 kB; note it in progress.md.
- [ ] **Step 2:** chrome-devtools: performance trace of a focus flight at 4× CPU throttle (≥ 30 fps, target 60); heap snapshot after switching between 10 colonies (no growing renderer or geometry count); 1 box and 6 boxes uncropped; tall Stammdaten (restriction hint shown) still uncropped; light and dark; emulated `prefers-reduced-motion` → no flights; 390 px.
- [ ] **Step 3:** Fix findings, gates green. Orchestrator commits `feat: holographic 3D hive scene (Beute L3)`.
## L4 — Umbauen
### Task 17: Editor route, draft with undo, palette, „Hinzufügen …“
**Files:**
- Create: `ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.tsx`, `src/components/hive/editor/use-hive-draft.ts`, `part-palette.tsx`, `add-part-dialog.tsx`, `editor-toolbar.tsx`
- Test: `use-hive-draft.spec.ts`, `part-palette.spec.tsx`, `colonies/[id]/hive/page.spec.tsx`
**Interfaces:**
- Consumes: Tasks 8–15, `useHiveMutation`.
- Produces: `useHiveDraft(base: HiveState, catalog): { state: HiveState; ops: HiveOp[]; apply(op: HiveOp): { ok: true } | { ok: false; reason: string }; undo(): void; redo(): void; canUndo: boolean; canRedo: boolean; reset(): void }` (state = `applyHiveOps(base, ops)`); palette groups Zargen / Gitter und Böden / Fütterung und Behandlung / Abdeckung und Deckel with active part types only; „Hinzufügen …“ dialog with a position select („über Zarge 1“) → `addPart` or `addInsert` (new boxes: all slots `FOUNDATION`); editor outline with „Nach oben“, „Nach unten“, „Entfernen“; toolbar with Rückgängig/Wiederholen (Ctrl+Z, Ctrl+Shift+Z), date (Berlin today), „Speichern“ (one `EDITOR` change), „Verwerfen“ (confirm when ops exist), „Als Vorlage speichern“ (only with `hive-systems:manage`: strips ids, names and levels, PATCHes the system `template`); leaving with ops asks (beforeunload and the in-app back link); zoom 0.6–2.5; offline page shows „Umbauen geht nur mit Netz“.
- [ ] **Step 1: Failing tests**: apply/undo/redo; adding a second floor shows the `RULE_FLOOR_BOTTOM` message inline; save sends all ops once; Verwerfen with ops asks; shortcuts work.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 18: Drag and drop in 3D
**Files:**
- Create: `ApiCore-Frontend/src/components/hive/editor/drop-slots.ts`, `use-hive-drag.ts`, `src/components/hive/hive-scene/drop-targets.tsx`
- Modify: `editor/part-palette.tsx`, `hive-scene/hive-canvas.tsx` (editor mode)
- Test: `drop-slots.spec.ts`, `use-hive-drag.spec.ts`
**Interfaces:**
- Produces: `dropSlots(state, catalog, dragged: { partTypeId: string } | { partId: string }): DropSlot[]`, `DropSlot = { kind: "stack"; at: number } | { kind: "insert"; hostId: string }` — a floor never moves; a roof only on top; FLOOR_INSERT only on floors without an insert; EVAPORATOR only on boxes without one. `useHiveDrag` with Pointer Events: on drag start the stack opens (+0.6 between parts), legal slots glow, the preview snaps to the nearest slot within 0.5 units, drop → `draft.apply(addPart | addInsert | movePart)`, drop on the „Entfernen“ zone → `removePart`.
- [ ] **Step 1: Failing tests**: template + dragged box → stack slots `at ∈ {1, 2, 3, 4, 5}`; dragged Bausperre → one insert slot on the floor; dragged floor → no slots; dragged existing roof → no slots (it can only stay on top).
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
- [ ] **Step 5: Browser check** of drag with mouse and touch emulation; gates green. Orchestrator commits `feat: hive editor with drag and drop (Beute L4)`.
## L5 — Durchsicht und Offline
### Task 19: „Beute“ section in the inspection form
**Files:**
- Modify: `ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx`, `inspection-form-values.ts`, `src/lib/inspections/types.ts` (`CreateInspectionInput.hiveChange?: { ops: HiveOp[] }`)
- Test: `inspections/new/page.spec.tsx`, `inspection-form-values.spec.ts`
**Interfaces:**
- Consumes: `<HiveCard mode="inspection" onOpsChange>`, `describeHiveOps`, `hiveFollowUps`, `deriveHiveFacts`.
- Produces: section „Beute“ (edit controls only with `colonies:update`, otherwise the model read-only); list „Änderungen an der Beute (n)“ with „Rückgängig“ per entry; hint „Laut Beute: 6 Brutwaben“ + „Übernehmen“ under `broodFrames` (never auto-filled); payload has `hiveChange` only when ops exist; follow-up toasts after saving.
- [ ] **Step 1: Failing tests**: ops appear in the list and the payload; Rückgängig removes one; the hint fills `broodFrames` only after the click; without permission no edit controls.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 20: Offline read (pack, service worker, offline colony page)
**Files:**
- Modify: `ApiCore-Backend/src/apiaries/apiary-field.types.ts` and the offline-pack builder (optional `hives?: Record<string, HiveSetupView>`, `hiveCatalog?: HiveSystemDto[]`), `ApiCore-Frontend/src/lib/apiaries/field-types.ts` (same optional fields), `public/sw-routes.js` (add `/\/api\/v1\/colonies\/[^/]+\/hive\/?$/i` to `NEVER_CACHED_API_PATHS`), `public/sw.js` (`SW_ROUTES_VERSION`), `src/lib/hive/api.ts` (`fetchHive` via `withPackFallback`; `fetchHiveSystems` falls back to `hiveCatalog`), `src/app/(app)/offline/colony/page.tsx` (read-only SVG + outline, or „Noch keine Beute angelegt“)
- Test: backend offline-pack spec, `sw-routes.spec.ts`, `sw.spec.ts`, `lib/hive/api.spec.ts`, `offline/colony/page.spec.tsx`
- [ ] **Step 1: Failing tests**: the pack contains the hives of the apiary's colonies; a network error returns the hive from the pack; `/api/v1/colonies/x/hive` is never cached and `/api/v1/colonies/x/hive/changes` is not matched by the new rule; offline colony page with and without setup (Review Focus 4).
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 21: Offline write (`pending-hive-changes`, `drainHiveTimeline`)
**Files:**
- Modify: `ApiCore-Frontend/src/lib/offline/db.ts` (`DB_VERSION = 5`, store `pending-hive-changes` with input `{ colonyId: string; occurredOn: string; source: "CARD"; ops: HiveOp[] }` in `RecordQueueInputs`), `queue-texts.ts` (label „Beute-Änderung“, texts for 409 `HIVE_CONFLICT` and 403 from spec §11), `sync.ts` (`drainHiveTimeline`; the inspection drain skips entries with `hiveChange`), the queue sheet component („Ohne Beute-Änderung senden“, „Verwerfen“), `src/components/hive/use-hive-mutation.ts` (offline → enqueue)
- Create: `ApiCore-Frontend/src/lib/hive/pending.ts` (`pendingHiveOps(colonyId: string, ownerUserId: string): Promise<HiveOp[]>` from both stores in `queuedAt` order)
- Test: `sync.spec.ts`, `db.spec.ts` (upgrade 4 → 5 keeps stores and entries), `queue-texts.spec.ts`, `lib/hive/pending.spec.ts`, `hive-card.spec.tsx` (overlay „1 Änderung wartet auf Netz“)
**Interfaces:**
- Produces: `drainHiveTimeline(ownerUserId: string): Promise<void>` — merges both sources sorted by `queuedAt`, sends one by one; network error or 5xx stops and retries later; 409 `HIVE_CONFLICT` or 403 sets `lastError` and holds later entries of the same colony; other 409 (idempotency) as today; „Ohne Beute-Änderung senden“ removes `hiveChange` and resends with the same `Idempotency-Key`.
- [ ] **Step 1: Failing tests**: order across both stores; held entries after a conflict (Review Focus 1); strip-and-resend; DB upgrade; the card overlay applies pending ops.
- [ ] **Step 2: Run** → FAIL. **Step 3: Implement.** **Step 4: Run** → PASS.
### Task 22: Final check and docs
- [ ] **Step 1:** Full gates backend and frontend.
- [ ] **Step 2:** Browser check per spec §12.2 (1280/390, light/dark, reduced motion, keyboard; DevTools offline: card edit queued then synced; inspection with hive ops offline).
- [ ] **Step 3:** `ARCHITECTURE.md` §2 offline (never-cached path, pack fields, `drainHiveTimeline`), `TASKS.md` L2–L5 ticked; `code-simplifier` pass on the changed code; whole-diff review.
- [ ] **Step 4:** Orchestrator commits `feat: hive in inspections and offline (Beute L5)`, pushes all bundled commits, polls CI.