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,139 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import { berlinDayOf, dayToDate } from '../common/dates';
|
||||
import type { SeasonState } from '../season/season-types';
|
||||
import { utcDay } from '../domain-events/domain-event-types';
|
||||
import type { AutopilotInput } from './autopilot.types';
|
||||
|
||||
/**
|
||||
* Until Task 27 passes the real start of the late summer, the feedings and
|
||||
* treatments of A4-A6 are counted from this day of the year (the built-in
|
||||
* calendar day of TRACHTENDE).
|
||||
*/
|
||||
const LATE_SUMMER_FALLBACK_START = '08-01';
|
||||
|
||||
const LATEST_FIRST = [{ date: 'desc' }, { createdAt: 'desc' }] as const;
|
||||
|
||||
function lateSummerStart(season: SeasonState): string | null {
|
||||
return season.segment === 'SPAETSOMMER' || season.segment === 'HERBST'
|
||||
? `${season.year}-${LATE_SUMMER_FALLBACK_START}`
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What the autopilot rules need of one colony, in a fixed sequence of
|
||||
* queries (no query per rule, none per record). Resolves to null for a
|
||||
* colony that does not exist. The caller holds the colony row lock, so the
|
||||
* picture stays consistent while the rules run.
|
||||
*/
|
||||
export async function loadAutopilotInputIn(
|
||||
tx: Prisma.TransactionClient,
|
||||
colonyId: string,
|
||||
season: SeasonState,
|
||||
): Promise<AutopilotInput | null> {
|
||||
const colony = await tx.colony.findUnique({
|
||||
where: { id: colonyId },
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
status: true,
|
||||
colonyType: true,
|
||||
apiaryId: true,
|
||||
createdAt: true,
|
||||
feedStatus: true,
|
||||
currentQueen: { select: { id: true, hatchYear: true } },
|
||||
},
|
||||
});
|
||||
if (colony === null) {
|
||||
return null;
|
||||
}
|
||||
const inspection = await tx.inspection.findFirst({
|
||||
where: { colonyId },
|
||||
orderBy: [...LATEST_FIRST],
|
||||
select: {
|
||||
id: true,
|
||||
date: true,
|
||||
colonyStrength: true,
|
||||
feedStatus: true,
|
||||
queenCellsPresent: true,
|
||||
},
|
||||
});
|
||||
const varroa = await tx.varroaMeasurement.findFirst({
|
||||
where: { colonyId },
|
||||
orderBy: [...LATEST_FIRST],
|
||||
select: { id: true, date: true },
|
||||
});
|
||||
// The latest END day wins, not the latest entry: a backdated or
|
||||
// overlapping treatment must not hide one that ends later.
|
||||
const treatments = await tx.treatment.findMany({
|
||||
where: { colonyId },
|
||||
orderBy: [{ date: 'desc' }, { id: 'asc' }],
|
||||
select: { id: true, date: true, startDate: true, endDate: true },
|
||||
});
|
||||
const lastTreatment = treatments
|
||||
.map((treatment) => ({
|
||||
id: treatment.id,
|
||||
startDay: utcDay(treatment.startDate ?? treatment.date),
|
||||
endDay: utcDay(treatment.endDate ?? treatment.date),
|
||||
}))
|
||||
.reduce<{ id: string; startDay: string; endDay: string } | null>(
|
||||
(latest, candidate) =>
|
||||
latest === null || candidate.endDay > latest.endDay
|
||||
? candidate
|
||||
: latest,
|
||||
null,
|
||||
);
|
||||
const varroaSinceLastTreatmentEnd =
|
||||
lastTreatment !== null &&
|
||||
(await tx.varroaMeasurement.count({
|
||||
where: { colonyId, date: { gt: dayToDate(lastTreatment.endDay) } },
|
||||
})) > 0;
|
||||
const feedingsSinceInspection = await tx.feeding.count({
|
||||
where: {
|
||||
colonyId,
|
||||
...(inspection === null ? {} : { date: { gte: inspection.date } }),
|
||||
},
|
||||
});
|
||||
const lateSummer = lateSummerStart(season);
|
||||
const sinceLateSummer =
|
||||
lateSummer === null ? null : { gte: dayToDate(lateSummer) };
|
||||
const feedingsSinceLateSummer =
|
||||
sinceLateSummer === null
|
||||
? 0
|
||||
: await tx.feeding.count({ where: { colonyId, date: sinceLateSummer } });
|
||||
const treatmentsSinceLateSummer =
|
||||
sinceLateSummer === null
|
||||
? 0
|
||||
: await tx.treatment.count({
|
||||
where: { colonyId, date: sinceLateSummer },
|
||||
});
|
||||
const winterChecks = await tx.winterCheck.findMany({
|
||||
where: { colonyId, winterYear: { gte: season.year - 1 } },
|
||||
select: { winterYear: true, season: true },
|
||||
orderBy: [{ winterYear: 'asc' }, { season: 'asc' }],
|
||||
});
|
||||
return {
|
||||
colony: {
|
||||
id: colony.id,
|
||||
code: colony.code,
|
||||
status: colony.status,
|
||||
colonyType: colony.colonyType,
|
||||
apiaryId: colony.apiaryId,
|
||||
createdOn: berlinDayOf(colony.createdAt),
|
||||
feedStatus: colony.feedStatus,
|
||||
},
|
||||
lastInspection:
|
||||
inspection === null
|
||||
? null
|
||||
: { ...inspection, date: utcDay(inspection.date) },
|
||||
lastVarroa:
|
||||
varroa === null ? null : { id: varroa.id, date: utcDay(varroa.date) },
|
||||
lastTreatment,
|
||||
varroaSinceLastTreatmentEnd,
|
||||
feedingsSinceInspection,
|
||||
feedingsSinceLateSummer,
|
||||
treatmentsSinceLateSummer,
|
||||
queen: colony.currentQueen,
|
||||
winterChecks,
|
||||
season,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user