- 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]>
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
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,
|
|
};
|
|
}
|