Files
ApiCore/ApiCore-Frontend/src/lib/hive/facts.ts
T
Nick SzaunigandClaude Opus 5.5 c608f5d405 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]>
2026-10-16 09:12:03 +02:00

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,
};
}