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
@@ -0,0 +1,112 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import { berlinToday } from '../common/dates';
|
||||
import type {
|
||||
AnyDomainEventEnvelope,
|
||||
DomainEventType,
|
||||
} from '../domain-events/domain-event-types';
|
||||
import { OnDomainEvent } from '../domain-events/on-domain-event.decorator';
|
||||
import { AutopilotService } from './autopilot.service';
|
||||
|
||||
const COLONY_SYNC_EVENTS = [
|
||||
'InspectionRecorded',
|
||||
'InspectionCorrected',
|
||||
'VarroaMeasured',
|
||||
'VarroaCorrected',
|
||||
'TreatmentRecorded',
|
||||
'TreatmentCorrected',
|
||||
'FeedingRecorded',
|
||||
'FeedingCorrected',
|
||||
'HarvestRecorded',
|
||||
'HarvestCorrected',
|
||||
'ColonyStatusChanged',
|
||||
'ColonySplit',
|
||||
'QueenAssigned',
|
||||
'TaskCompleted',
|
||||
'TaskUpdated',
|
||||
'WithdrawalEnded',
|
||||
'QueenAged',
|
||||
'InspectionOverdue',
|
||||
'VarroaMeasurementDue',
|
||||
] as const satisfies readonly DomainEventType[];
|
||||
|
||||
type ColonySyncEvent = AnyDomainEventEnvelope<
|
||||
(typeof COLONY_SYNC_EVENTS)[number]
|
||||
>;
|
||||
|
||||
/** The colony of a task, if it is about one. */
|
||||
async function taskColonyIdIn(
|
||||
tx: Prisma.TransactionClient,
|
||||
taskId: string,
|
||||
): Promise<string | null> {
|
||||
const task = await tx.task.findUnique({
|
||||
where: { id: taskId },
|
||||
select: { relatedType: true, relatedId: true },
|
||||
});
|
||||
return task?.relatedType === 'COLONY' ? task.relatedId : null;
|
||||
}
|
||||
|
||||
/** The colonies an event is about; none if it is about no colony. */
|
||||
async function colonyIdsOf(
|
||||
event: ColonySyncEvent,
|
||||
tx: Prisma.TransactionClient,
|
||||
): Promise<(string | null)[]> {
|
||||
switch (event.type) {
|
||||
case 'InspectionRecorded':
|
||||
case 'InspectionCorrected':
|
||||
case 'VarroaMeasured':
|
||||
case 'VarroaCorrected':
|
||||
case 'TreatmentRecorded':
|
||||
case 'TreatmentCorrected':
|
||||
case 'FeedingRecorded':
|
||||
case 'FeedingCorrected':
|
||||
case 'WithdrawalEnded':
|
||||
case 'QueenAged':
|
||||
return [event.payload.colonyId];
|
||||
case 'HarvestRecorded':
|
||||
case 'HarvestCorrected':
|
||||
return event.payload.colonyIds;
|
||||
case 'ColonyStatusChanged':
|
||||
case 'InspectionOverdue':
|
||||
case 'VarroaMeasurementDue':
|
||||
return [event.aggregateId];
|
||||
case 'ColonySplit':
|
||||
// The aggregate is the new colony.
|
||||
return [event.aggregateId, event.payload.parentColonyId];
|
||||
case 'QueenAssigned':
|
||||
return [event.payload.colonyId, event.payload.previousColonyId];
|
||||
case 'TaskCompleted':
|
||||
return [
|
||||
event.payload.relatedType === 'COLONY' ? event.payload.relatedId : null,
|
||||
];
|
||||
case 'TaskUpdated':
|
||||
return [await taskColonyIdIn(tx, event.aggregateId)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconciles the autopilot suggestions of the colonies an event touches.
|
||||
* Pure DB writer through the delivery transaction; the colonies are synced
|
||||
* in id order, so two deliveries never lock them the other way round.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AutopilotEventHandlers {
|
||||
constructor(private readonly autopilot: AutopilotService) {}
|
||||
|
||||
@OnDomainEvent([...COLONY_SYNC_EVENTS], 'autopilot.colony-sync')
|
||||
async colonySync(
|
||||
event: ColonySyncEvent,
|
||||
tx: Prisma.TransactionClient,
|
||||
): Promise<void> {
|
||||
const colonyIds = [
|
||||
...new Set((await colonyIdsOf(event, tx)).filter((id) => id !== null)),
|
||||
].sort();
|
||||
if (colonyIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const today = berlinToday();
|
||||
for (const colonyId of colonyIds) {
|
||||
await this.autopilot.syncColonyIn(tx, colonyId, today);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user