feat: T4 wave 4 - rules suggest, takeover, chains, autopilot A1-A3, suggestions page, varroa bands UI
- Automation rules propose suggestions instead of creating tasks; notify per rule config; corrected varroa measurements refresh or resolve their suggestion - Inspection values can be taken over into the colony card (occupiedFrames 0-40, strength deprecated); warning counts use the same visibility rule as the notification list - Biotech chains: swarm check after queen cells, queen acceptance check, split schedules (raise own, queen cell, queen) - Season autopilot core with A1 inspection due, A2 varroa measurement, A3 recheck after treatment; colony sync and daily step - Frontend: suggestions page with groups, bulk accept, undo, paging, focus management and permission gating - Frontend: varroa traffic light from seasonal bands, sample size, one range request for the colony chart - Notifications: mark-read answers 404 for rows the user may no longer see Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
64a2b7dad9
commit
02eb16924e
142 files changed
+11847
-1251
No files matched your search
@@ -95,16 +95,30 @@ describe('Follow-up controls (e2e)', () => {
|
||||
},
|
||||
data: { active: false, config: {} },
|
||||
});
|
||||
// Follow-up tasks are written without a user: remove their audit entry
|
||||
// and TaskCreated (deliveries go with the event, ON DELETE CASCADE).
|
||||
const followUpTaskIds = (
|
||||
await prisma.task.findMany({
|
||||
where: { relatedId: { in: colonyIds } },
|
||||
// Follow-up suggestions are written without a user: remove them with
|
||||
// their audit entries (deliveries go with the event, ON DELETE CASCADE).
|
||||
const suggestionIds = (
|
||||
await prisma.suggestion.findMany({
|
||||
where: { colonyId: { in: colonyIds } },
|
||||
select: { id: true },
|
||||
})
|
||||
).map((task) => task.id);
|
||||
).map((suggestion) => suggestion.id);
|
||||
await prisma.auditLog.deleteMany({
|
||||
where: { entityType: 'Task', entityId: { in: followUpTaskIds } },
|
||||
where: {
|
||||
OR: [
|
||||
{ entityType: 'Suggestion', entityId: { in: suggestionIds } },
|
||||
...colonyIds.map((id) => ({
|
||||
entityType: 'AutomationRule',
|
||||
afterData: { path: ['relatedId'], equals: id },
|
||||
})),
|
||||
],
|
||||
},
|
||||
});
|
||||
await prisma.notification.deleteMany({
|
||||
where: { relatedType: 'Suggestion', relatedId: { in: suggestionIds } },
|
||||
});
|
||||
await prisma.suggestion.deleteMany({
|
||||
where: { id: { in: suggestionIds } },
|
||||
});
|
||||
const measureIds = [
|
||||
...(await prisma.treatment.findMany({
|
||||
@@ -122,9 +136,6 @@ describe('Follow-up controls (e2e)', () => {
|
||||
await prisma.auditLog.deleteMany({
|
||||
where: { entityId: { in: measureIds } },
|
||||
});
|
||||
await prisma.domainEvent.deleteMany({
|
||||
where: { aggregateType: 'Task', aggregateId: { in: followUpTaskIds } },
|
||||
});
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -136,9 +147,9 @@ describe('Follow-up controls (e2e)', () => {
|
||||
|
||||
const colony = await newColony('OFF');
|
||||
await treat(colony.id);
|
||||
expect(await prisma.task.count({ where: { relatedId: colony.id } })).toBe(
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
await prisma.suggestion.count({ where: { colonyId: colony.id } }),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('suggests a check due the configured days after the treatment ended', async () => {
|
||||
@@ -155,25 +166,30 @@ describe('Follow-up controls (e2e)', () => {
|
||||
endDate: '2026-08-10T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const tasks = await prisma.task.findMany({
|
||||
where: {
|
||||
relatedId: colony.id,
|
||||
sourceAutomationTrigger: 'TREATMENT_FOLLOW_UP',
|
||||
},
|
||||
const suggestions = await prisma.suggestion.findMany({
|
||||
where: { colonyId: colony.id, ruleKey: 'TREATMENT_FOLLOW_UP' },
|
||||
});
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].dueDate?.toISOString()).toBe('2026-08-24T00:00:00.000Z');
|
||||
expect(tasks[0].description).toContain('Testmittel');
|
||||
expect(tasks[0].status).toBe('OPEN');
|
||||
expect(suggestions).toHaveLength(1);
|
||||
expect(suggestions[0]).toMatchObject({
|
||||
status: 'OPEN',
|
||||
intent: 'FOLLOW_UP',
|
||||
reasonCode: 'TREATMENT_FOLLOW_UP',
|
||||
requiredPermission: 'treatments:read',
|
||||
evidence: { colonyId: colony.id, endedOn: '2026-08-10' },
|
||||
});
|
||||
expect(suggestions[0].dueDate?.toISOString()).toBe(
|
||||
'2026-08-24T00:00:00.000Z',
|
||||
);
|
||||
// The rule proposes; no task exists before the suggestion is accepted.
|
||||
expect(await prisma.task.count({ where: { relatedId: colony.id } })).toBe(
|
||||
0,
|
||||
);
|
||||
|
||||
// A second treatment while the check is still open adds nothing.
|
||||
await treat(colony.id, { date: '2026-08-12T00:00:00.000Z' });
|
||||
expect(
|
||||
await prisma.task.count({
|
||||
where: {
|
||||
relatedId: colony.id,
|
||||
sourceAutomationTrigger: 'TREATMENT_FOLLOW_UP',
|
||||
},
|
||||
await prisma.suggestion.count({
|
||||
where: { colonyId: colony.id, ruleKey: 'TREATMENT_FOLLOW_UP' },
|
||||
}),
|
||||
).toBe(1);
|
||||
});
|
||||
@@ -199,17 +215,15 @@ describe('Follow-up controls (e2e)', () => {
|
||||
})
|
||||
.expect(201);
|
||||
// Not before the FeedingRecorded handler ran.
|
||||
expect(await prisma.task.count({ where: { relatedId: colony.id } })).toBe(
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
await prisma.suggestion.count({ where: { colonyId: colony.id } }),
|
||||
).toBe(0);
|
||||
await drainDomainEvents(app, [(response.body as { id: string }).id]);
|
||||
|
||||
const task = await prisma.task.findFirstOrThrow({
|
||||
where: {
|
||||
relatedId: colony.id,
|
||||
sourceAutomationTrigger: 'FEEDING_FOLLOW_UP',
|
||||
},
|
||||
const suggestion = await prisma.suggestion.findFirstOrThrow({
|
||||
where: { colonyId: colony.id, ruleKey: 'FEEDING_FOLLOW_UP' },
|
||||
});
|
||||
expect(task.dueDate?.toISOString()).toBe('2026-09-08T00:00:00.000Z');
|
||||
expect(suggestion.dueDate?.toISOString()).toBe('2026-09-08T00:00:00.000Z');
|
||||
expect(suggestion.evidence).toMatchObject({ fedOn: '2026-09-01' });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user