Files
2026-10-15 20:00:33 +02:00

40 KiB
Raw Permalink Blame History

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.