Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
40 KiB
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 alsonpm run test:e2e(test DBlocalhost: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 writescolonies:update; catalog writeshive-systems:manage(new, system role ADMIN via catalog sync, no grant migration). - Days are Berlin calendar days
YYYY-MM-DD(berlinToday,berlinDayOfinsrc/common/dates.ts);occurredOnnever 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; 409HIVE_EXISTSfor a second setup; 409 likeassertActivefor non-living colonies. - Limits: ≤ 100 ops per request, ≤ 30 parts in the stack, labels and frame names ≤ 40 chars.
- Lock order:
ColonyFOR UPDATE, thenHiveSetupFOR UPDATE (HiveSetupdirectly afterColonyinLOCKABLE_TABLES). - Hive facts never write into
ColonyorInspection.Colony.occupiedFramesstays 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/offplusprefers-reduced-motionviauseEffectiveMotionLevel();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-auditorskill 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
- Offline edit, then someone else removes the part: queued
setFrameon a removed box → 409 HIVE_CONFLICT, queue shows the entry with „Verwerfen“, later entries of that colony wait. Test in Task 21. - 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.
- 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.
- Colony without setup opened offline:
/offline/colonyand the card must show „Noch keine Beute angelegt“ read-only, not crash. Test in Task 20. - 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(modelsHiveSystem,HivePartType,HiveSetup,HiveChange, enumsHivePartKind,HiveBoxDepth,HiveChangeSource; back-relationsColony.hiveSetup,Inspection.hiveChange,User.hiveChanges) — exactly spec §5.1. - Create:
ApiCore-Backend/prisma/migrations/20261017100000_hive_setup/migration.sql(tables) and20261017100100_hive_catalog_zander_liebig/migration.sql(seed rows). - Modify:
ApiCore-Backend/src/prisma/row-lock.ts(HiveSetup: lockable('HiveSetup', 'colonyId')directly afterColony). - Modify:
ApiCore-Backend/src/auth/permissions/permission-catalog.ts(group „Beutensysteme“, keyhive-systems, actionmanage, 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 typeshive-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 prefixedhive-part-zl10-), dimensions from spec §4.3. -
State schemas: floor
entrance(OPEN/NARROW/CLOSED, „offen/eng/zu“),varroaTray(IN/OUT, „drin/draußen“); dividerbungHoles(OPEN/CLOSED/BEE_ESCAPE, „offen/zu/Bienenflucht“); inner lidfeedHole(OPEN/CLOSED, „offen/zu“); feedersyrupLiters(l, max 3, step 0.5, feedType SUGAR_WATER) andfondantKg(kg, max 2.5, step 0.5, feedType FEED_PASTE); evaporatoracidMl(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: systemhive-system-zander-liebig-10exists with 14 active part types;hive-part-zl10-box-10hasframeSlots 10,heightMm 230; feederstateSchemahas twolevelfields withfeedType;hive-systems:manageis inPERMISSION_CATALOGand 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 typecheckclean.
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,atclamping, duplicate id, foreign and inactivepartTypeId) - Test:
ApiCore-Backend/src/hive/hive-ops.spec.ts,hive-facts.spec.ts
Interfaces:
-
Produces:
- Types
HiveState,HivePart,HiveFrame,FrameType,HiveOpexactly 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 | ChoiceFieldper 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 }.
- Types
-
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_TYPEfollowed 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/addInsertwith one failsINACTIVE_PART_TYPE;atoutside[0, stack.length]is clamped; a level value must be a multiple ofstep(tolerance 1e-9). -
Step 1: Write fixtures and
hive-ops.spec.tslooping over every case file: result matchesexpect; 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 withfeedType "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.tsandsrc/all-in-one.module.tswhere 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>(freshrandomUUID()per part).- Routes of spec §6.2 with
@RequirePermission; catalog writes recorded withauditLog.recordlike other master data.
-
Part-type updates may only append
stateSchemafields (otherwise 400SCHEMA_NOT_ADDITIVE); a systemtemplatemust passvalidateHiveState. -
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 setsactive 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 }, aggregateColony),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>; 404HIVE_NOT_FOUNDwithout setup.HiveService.create(colonyId, { systemId, state, occurredOn }, userId): Promise<HiveSetupView>→ version 1 plusHiveChangesource 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 andoccurredOn; applies ops; failure throwsHiveConflictException(409 body of Global Constraints); writesHiveChange(version + 1,stateAfter), updates the setup, recordsHiveChanged. 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 }, orderedoccurredOn desc, version desc.HiveService.at(colonyId, day: string): Promise<{ version; state; facts; occurredOn }>per spec §6.2 (404 before INIT).- Controller:
POST /colonies/:id/hiveandPOST /colonies/:id/hive/changeswith@Idempotent(); DTOsourceaccepts onlyCARD | 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 withcurrent;occurredOntomorrow (Berlin) → 400; before INIT → 400; version increments;HiveChangedwritten in the same transaction (a rolled-back transaction leaves no event);atreturns the highest version withoccurredOn <= dayand 404 before INIT; a backdated change sorts byoccurredOnin 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(importHiveModule) - Test:
ApiCore-Backend/src/inspections/inspections.service.spec.ts
Interfaces:
-
Consumes: Task 4
HiveService.applyChange. -
Produces:
create()destructureshiveChangetogether withapplyToColony; checkscolonies:updatebefore the transaction whenhiveChangeis present (403HIVE_FORBIDDENotherwise); locks Colony before the inspection insert whenhiveChangeorapplyToColonyis present; callsapplyChangewithsource INSPECTION,occurredOn = berlinDayOf(new Date(dto.date)),inspectionId; response addshive?: HiveSetupView. -
Step 1: Write failing tests: inspection and ops saved together (one
HiveChangewithinspectionId); a conflicting op leaves no inspection row; a user withoutcolonies:updategets 403 and no row; withouthiveChangethe 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 existingHiveChangeandHiveSetup, then insert → after inspection:hiveChangewithuserId: resolveUserId(...). Sealed backups only (not inLEGACY_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 -- backupandnpm 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 withouthive-systems:manage403, with it 201; template; create; change; idempotent replay returns the same body; history cursor;at; inspection withhiveChange(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 withit.skipIf(!existsSync(dir))likelib/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): HiveCatalogwith 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"].
- Types mirroring Tasks 2/4 (
-
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 propdecimals?: number, default 0; withdecimals > 0accept,and., display German comma, values on thestepgrid) -
Modify:
ApiCore-Frontend/src/lib/motion/motion-preference.ts(useEffectiveMotionLevel(): MotionLevel, listening to the reduce-motion media querychange,storageevents and an in-appapicore:motionevent dispatched bysetLevel) -
Test:
number-stepper.spec.tsx,motion-preference.spec.ts -
Step 1: Failing tests: with
decimals={1} step={0.5}typing „2,5“ callsonChange("2.5"); „+“ from „2,5“ shows „3“; „2,25“ shows the field error „Nur Schritte von 0,5.“; withoutdecimalsthe existing specs stay green. Hook returnssubtlewhen reduce-motion matches and the stored level isfull, and updates after the mediachangeevent. -
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 withdata-part-id, frames only foropenBoxIdwith a pattern per type and the type word; strokescurrentColorforforced-colors.<HiveOutline state catalog canEdit announcement onEditPart(id) onEditFrame(boxId, slot) />:role="tree", roof first, boxes expandable, frames astreeitem„R4 · Brutwabe · Name“; arrows move, Enter opens the box or the edit sheet; focus callssetHovered;announcementgoes to a polite live region.
-
Step 1: Failing tests: outline order roof → floor; Down/Up/Right/Left/Enter behaviour; focus sets
hoveredId; SVG has oneg[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.tsandsrc/app/(app)/treatments/new/treatment-form-values.tsplus their pages (read optional search paramsdate,amount,unit,feedTypeinto 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 helperlib/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 409HIVE_CONFLICTsetsconflict = { 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“ → onesetFrame.<PartSheet>: one control perstateSchemafield (stepper withdecimalsderived fromstep, ChoiceGroup for choice); floor also „Bausperre einlegen/entfernen“; box also „Verdunster einsetzen/entfernen“; „Speichern“ → ops.
-
Step 1: Failing tests: FrameSheet saves
setFramewith the chosen type and trimmed name; PartSheet feeder 2 → 5 l sendssetStateand shows „3 l Flüssigfutter nachgefüllt. Als Fütterung eintragen?“ with the prefilled link; 409 shows the conflict preview; the feeding form readsamount=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 1lg:grid-cols-3withoutitems-start:ColonyEditCard+HiveCardwithlg:col-span-2; row 2 withitems-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[]) />: loadsfetchHive; 404 →<HiveInit>(template preview, „Übernehmen“, „Wie anderes Volk“ with colony select; controls only withcolonies:update); otherwiseHiveViewProvider+ visual slot (<HiveSvg>now,HiveVisualfrom 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 throughonOpsChangeinstead of saving (Task 19). Card bodyflex h-full min-h-0 flex-col; belowlgheightmin(55svh,520px), minimum 320 px.<HiveHistory colonyId onShowVersion(state: HiveState | null) />: paged list withdescribeHiveOpstext, „nachgetragen am …“ whenoccurredOnis before thecreatedAtday, follow-up buttons fromhiveFollowUps; 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.tssettings section), visible only withhive-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,
frameSlotsfor boxes, appendlevel/choicefields, 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-fgin:rootand.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 withoffand ≤ 2 cores);<HiveVisual layout catalog onSelectPart onSelectFrame />renders<HiveSvg>at once and, when the card enters the viewport andshouldUse3Dholds, loadsHiveCanvasvianext/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-auditorskill on the eight packages; note the result in.superpowers/sdd/2026-10-15-beute-3d/progress.md; install withnpm install --save-exact. -
Step 2: Failing tests:
shouldUse3Dcases above;HiveVisualshows SVG first and never imports the scene whenshouldUse3Dis false (mockednext/dynamic). -
Step 3: Run → FAIL. Step 4: Implement. Step 5: Run → PASS;
npm run buildputs 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 framesplate; hovered frame lift 0.4 × frame height. The caller animates toward targets only atfull, otherwise sets them (with a 150 ms opacity fade atsubtle).<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 atfull); facesMeshBasicMaterialopacity 0.06–0.12 additive; drei<Edges>coloured from--holo-edge(read once bytheme.ts);EffectComposer+Bloomat half resolution with a high luminance threshold; floor grid; motion withmaath/easing.damp3;FillVolumewithsinvertex wobble atfull; 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
openBoxIdis set; pointer over a part callssetHovered(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-ykeeps vertical page scroll) and springs back after 4 s; idle float ±0.04 over 6 s and pointer tilt ≤ 6° only atfulland 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:
useTouchSelectfirst tap hovers, second tap selects, tap elsewhere resets; Esc closes the open box; atsubtleno 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“) →addPartoraddInsert(new boxes: all slotsFOUNDATION); editor outline with „Nach oben“, „Nach unten“, „Entfernen“; toolbar with Rückgängig/Wiederholen (Ctrl+Z, Ctrl+Shift+Z), date (Berlin today), „Speichern“ (oneEDITORchange), „Verwerfen“ (confirm when ops exist), „Als Vorlage speichern“ (only withhive-systems:manage: strips ids, names and levels, PATCHes the systemtemplate); 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_BOTTOMmessage 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.useHiveDragwith 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“ underbroodFrames(never auto-filled); payload hashiveChangeonly 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
broodFramesonly 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.tsand the offline-pack builder (optionalhives?: 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\/?$/itoNEVER_CACHED_API_PATHS),public/sw.js(SW_ROUTES_VERSION),src/lib/hive/api.ts(fetchHiveviawithPackFallback;fetchHiveSystemsfalls back tohiveCatalog),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/hiveis never cached and/api/v1/colonies/x/hive/changesis 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, storepending-hive-changeswith input{ colonyId: string; occurredOn: string; source: "CARD"; ops: HiveOp[] }inRecordQueueInputs),queue-texts.ts(label „Beute-Änderung“, texts for 409HIVE_CONFLICTand 403 from spec §11),sync.ts(drainHiveTimeline; the inspection drain skips entries withhiveChange), 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 inqueuedAtorder) - 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 byqueuedAt, sends one by one; network error or 5xx stops and retries later; 409HIVE_CONFLICTor 403 setslastErrorand holds later entries of the same colony; other 409 (idempotency) as today; „Ohne Beute-Änderung senden“ removeshiveChangeand resends with the sameIdempotency-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.mdL2–L5 ticked;code-simplifierpass 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.