docs: Beute-3D implementation plan (L1-L5)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
d03391d2f2
commit
8982d0796a
1 file changed
+377
@@ -0,0 +1,377 @@
|
|||||||
|
# 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.
|
||||||
Reference in new issue
Block a user