feat: hive card with SVG, outline, edit sheets and catalog settings (Beute L2)
- lib/hive: frontend port of the hive domain (ops, validator, facts, layout, follow-ups, describe texts, reason texts) running the backend fixtures, API client - hive card on the colony page (same height as Stammdaten): SVG view, accessible outline tree, frame/part sheets with optimistic save, conflict sheet, history with past states, init from template or another colony, inspection draft mode - follow-up toasts prefilling feeding and treatment forms (treatment end only with an open treatment) - settings page "Beutensysteme" (hive-systems:manage) - NumberStepper decimal steps, reactive motion level hook, holo tokens - spec: owner design decision 2026-10-16 (glass hologram, focus mode) The SVG view is the fallback; the 3D scene follows in L3. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
0091b3d28b
commit
c608f5d405
87 files changed
+9493
-43
No files matched your search
@@ -0,0 +1,74 @@
|
||||
import type {
|
||||
CatalogPartType,
|
||||
HiveCatalog,
|
||||
HiveFacts,
|
||||
HiveLevelFact,
|
||||
HivePart,
|
||||
HiveState,
|
||||
HiveSystemDto,
|
||||
} from "./types";
|
||||
|
||||
const UNBUILT_FRAME_TYPES = new Set(["NONE", "FOUNDATION", "DIVIDER"]);
|
||||
|
||||
/** Catalog of a system as the API returns it (all part types, also inactive). */
|
||||
export function catalogFromSystem(system: HiveSystemDto): HiveCatalog {
|
||||
const rows: CatalogPartType[] = system.partTypes.map((row) => ({
|
||||
id: row.id,
|
||||
kind: row.kind,
|
||||
name: row.name,
|
||||
lengthMm: row.lengthMm,
|
||||
widthMm: row.widthMm,
|
||||
heightMm: row.heightMm,
|
||||
frameSlots: row.frameSlots,
|
||||
stateSchema: row.stateSchema,
|
||||
active: row.active,
|
||||
}));
|
||||
return { systemId: system.id, partTypes: new Map(rows.map((row) => [row.id, row])) };
|
||||
}
|
||||
|
||||
function allParts(state: HiveState): HivePart[] {
|
||||
return state.stack.flatMap((part) => [part, ...(part.inserts ?? [])]);
|
||||
}
|
||||
|
||||
function levelsOf(part: HivePart, catalog: HiveCatalog): HiveLevelFact[] {
|
||||
const type = catalog.partTypes.get(part.partTypeId);
|
||||
if (!type) return [];
|
||||
return type.stateSchema.flatMap((field) => {
|
||||
if (field.kind !== "level") return [];
|
||||
const raw = part.state[field.key];
|
||||
return [
|
||||
{
|
||||
partId: part.id,
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
unit: field.unit,
|
||||
value: typeof raw === "number" ? raw : 0,
|
||||
max: field.max,
|
||||
...(field.feedType ? { feedType: field.feedType } : {}),
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/** Pure derivation of display facts (spec 5.3), same as the backend. */
|
||||
export function deriveHiveFacts(state: HiveState, catalog: HiveCatalog): HiveFacts {
|
||||
const parts = allParts(state);
|
||||
const kindOf = (part: HivePart) => catalog.partTypes.get(part.partTypeId)?.kind;
|
||||
const frames = state.stack
|
||||
.filter((part) => kindOf(part) === "BOX")
|
||||
.flatMap((part) => part.frames ?? []);
|
||||
const evaporators = parts.filter((part) => kindOf(part) === "EVAPORATOR");
|
||||
const acid = evaporators.reduce((sum, part) => {
|
||||
const value = part.state.acidMl;
|
||||
return sum + (typeof value === "number" ? value : 0);
|
||||
}, 0);
|
||||
return {
|
||||
boxCount: state.stack.filter((part) => kindOf(part) === "BOX").length,
|
||||
broodFrames: frames.filter((f) => f.type === "BROOD").length,
|
||||
honeyFrames: frames.filter((f) => f.type === "HONEY").length,
|
||||
builtFrames: frames.filter((f) => !UNBUILT_FRAME_TYPES.has(f.type)).length,
|
||||
levels: parts.flatMap((part) => levelsOf(part, catalog)),
|
||||
hasFloorInsert: parts.some((part) => kindOf(part) === "FLOOR_INSERT"),
|
||||
evaporatorAcidMl: evaporators.length > 0 ? acid : null,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user