- 3D dependencies (three, react-three-fiber, drei, postprocessing, motion), exact versions, supply-chain checked; scene loaded only on demand on the colony page (283 kB gzip), SVG stays as fallback - glass hologram per owner design 2026-10-16: true Liebig Zander shapes (grips, rebate, Wanderboden foot), cool blue edge glow, click any part -> it flies to the front while the rest stays as a faint ghost; focus shows content (feed levels, Bausperre, pockets); frames rise on hover and take their type look (colour + comb pattern) - full-bleed card with frosted overlays and glass buttons - Futtertasche (FRAME_FEEDER): new part kind occupying frame slots with a fill level, catalog row, validator rules in both ports, outline, sheets, follow-ups, settings Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
203 lines
5.7 KiB
TypeScript
203 lines
5.7 KiB
TypeScript
import { PERMISSION_CATALOG } from '../src/auth/permissions/permission-catalog';
|
|
import { PermissionCatalogSyncService } from '../src/auth/permissions/permission-catalog-sync.service';
|
|
import { PrismaService } from '../src/prisma/prisma.service';
|
|
|
|
const SYSTEM_ID = 'hive-system-zander-liebig-10';
|
|
const PART_PREFIX = 'hive-part-zl10-';
|
|
|
|
const PART_SLUGS = [
|
|
'floor',
|
|
'floor-insert',
|
|
'box-10',
|
|
'box-empty',
|
|
'divider-board',
|
|
'queen-excluder',
|
|
'feeder',
|
|
'cover-abdeckfix',
|
|
'cover-lotus',
|
|
'propolis-screen',
|
|
'inner-lid',
|
|
'insulation',
|
|
'roof',
|
|
'evaporator',
|
|
'frame-feeder',
|
|
] as const;
|
|
|
|
interface StateField {
|
|
key: string;
|
|
kind: string;
|
|
feedType?: string;
|
|
[extra: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* The Liebig-Zander start catalog arrives with the migration (fixed ids) and
|
|
* `hive-systems:manage` joins ADMIN through the boot-time catalog sync.
|
|
* Checked against the migrated test database.
|
|
*/
|
|
describe('Hive catalog seed (e2e)', () => {
|
|
let prisma: PrismaService;
|
|
|
|
beforeAll(() => {
|
|
prisma = new PrismaService();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
async function partType(slug: string) {
|
|
return prisma.hivePartType.findUniqueOrThrow({
|
|
where: { id: `${PART_PREFIX}${slug}` },
|
|
});
|
|
}
|
|
|
|
it('seeds the Zander-Liebig system with its geometry and template', async () => {
|
|
const system = await prisma.hiveSystem.findUniqueOrThrow({
|
|
where: { id: SYSTEM_ID },
|
|
});
|
|
|
|
expect(system).toMatchObject({
|
|
name: 'Zander nach Dr. Liebig, 10 Rähmchen',
|
|
frameWidthMm: 477,
|
|
frameHeightMm: 220,
|
|
footprintLMm: 525,
|
|
footprintWMm: 430,
|
|
active: true,
|
|
});
|
|
});
|
|
|
|
it('seeds 15 active part types for the system under fixed ids', async () => {
|
|
const types = await prisma.hivePartType.findMany({
|
|
where: { systemId: SYSTEM_ID, active: true },
|
|
orderBy: { id: 'asc' },
|
|
});
|
|
|
|
expect(types.map((t) => t.id)).toEqual(
|
|
PART_SLUGS.map((slug) => `${PART_PREFIX}${slug}`).sort(),
|
|
);
|
|
});
|
|
|
|
it('describes the 10-frame box with 10 slots and 230 mm height', async () => {
|
|
const box = await partType('box-10');
|
|
|
|
expect(box).toMatchObject({
|
|
kind: 'BOX',
|
|
boxDepth: 'FULL',
|
|
frameSlots: 10,
|
|
heightMm: 230,
|
|
lengthMm: 525,
|
|
widthMm: 430,
|
|
});
|
|
expect((await partType('box-empty')).frameSlots).toBe(0);
|
|
});
|
|
|
|
it('gives the feeder two level fields with a feed type each', async () => {
|
|
const feeder = await partType('feeder');
|
|
const fields = feeder.stateSchema as unknown as StateField[];
|
|
|
|
expect(fields).toHaveLength(2);
|
|
expect(fields.every((f) => f.kind === 'level')).toBe(true);
|
|
expect(fields.map((f) => [f.key, f.feedType])).toEqual([
|
|
['syrupLiters', 'SUGAR_WATER'],
|
|
['fondantKg', 'FEED_PASTE'],
|
|
]);
|
|
});
|
|
|
|
it('seeds the frame feeder over two frame slots with a 2 l syrup level', async () => {
|
|
const pocket = await partType('frame-feeder');
|
|
const fields = pocket.stateSchema as unknown as StateField[];
|
|
|
|
expect(pocket).toMatchObject({
|
|
kind: 'FRAME_FEEDER',
|
|
slotWidth: 2,
|
|
frameSlots: null,
|
|
lengthMm: 477,
|
|
widthMm: 70,
|
|
heightMm: 220,
|
|
});
|
|
expect(fields).toEqual([
|
|
{
|
|
key: 'syrupLiters',
|
|
label: 'Flüssigfutter',
|
|
kind: 'level',
|
|
unit: 'l',
|
|
max: 2,
|
|
step: 0.25,
|
|
feedType: 'SUGAR_WATER',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('gives the other stateful parts their choice and level fields', async () => {
|
|
const keysOf = async (slug: string) =>
|
|
((await partType(slug)).stateSchema as unknown as StateField[]).map(
|
|
(f) => f.key,
|
|
);
|
|
|
|
expect(await keysOf('floor')).toEqual(['entrance', 'varroaTray']);
|
|
expect(await keysOf('divider-board')).toEqual(['bungHoles']);
|
|
expect(await keysOf('inner-lid')).toEqual(['feedHole']);
|
|
expect(await keysOf('evaporator')).toEqual(['acidMl']);
|
|
expect(await keysOf('roof')).toEqual([]);
|
|
});
|
|
|
|
it('stores the system template without ids, with 10 foundation frames', async () => {
|
|
const system = await prisma.hiveSystem.findUniqueOrThrow({
|
|
where: { id: SYSTEM_ID },
|
|
});
|
|
const template = system.template as unknown as {
|
|
schemaVersion: number;
|
|
stack: {
|
|
id?: string;
|
|
partTypeId: string;
|
|
state: Record<string, unknown>;
|
|
frames?: { slot: number; type: string }[];
|
|
}[];
|
|
};
|
|
|
|
expect(template.schemaVersion).toBe(1);
|
|
expect(template.stack.map((p) => p.partTypeId)).toEqual([
|
|
`${PART_PREFIX}floor`,
|
|
`${PART_PREFIX}box-10`,
|
|
`${PART_PREFIX}cover-abdeckfix`,
|
|
`${PART_PREFIX}inner-lid`,
|
|
`${PART_PREFIX}insulation`,
|
|
`${PART_PREFIX}roof`,
|
|
]);
|
|
for (const part of template.stack) {
|
|
expect(part.id).toBeUndefined();
|
|
expect(part.state).toEqual({});
|
|
}
|
|
const frames = template.stack[1].frames ?? [];
|
|
expect(frames).toHaveLength(10);
|
|
expect(frames.map((f) => f.slot)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
expect(frames.every((f) => f.type === 'FOUNDATION')).toBe(true);
|
|
});
|
|
|
|
it('lists hive-systems:manage in the permission catalog', () => {
|
|
const entry = PERMISSION_CATALOG.find(
|
|
(e) => e.key === 'hive-systems:manage',
|
|
);
|
|
|
|
expect(entry).toMatchObject({
|
|
resource: 'hive-systems',
|
|
action: 'manage',
|
|
labelDe: 'Pflegen',
|
|
groupDe: 'Beutensysteme',
|
|
});
|
|
});
|
|
|
|
it('grants hive-systems:manage to ADMIN after the catalog sync', async () => {
|
|
await new PermissionCatalogSyncService(prisma).onModuleInit();
|
|
|
|
const grant = await prisma.rolePermission.findFirst({
|
|
where: {
|
|
role: { key: 'ADMIN' },
|
|
permission: { key: 'hive-systems:manage' },
|
|
},
|
|
});
|
|
expect(grant).not.toBeNull();
|
|
});
|
|
});
|